##// END OF EJS Templates
pull-requests: simplified the UI for pr view....
marcink -
r4136:12e6938f default
parent child Browse files
Show More
@@ -1,1217 +1,1215 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39
40 40 base_url = {
41 41 'repo_changelog': '/{repo_name}/changelog',
42 42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 43 'repo_commits': '/{repo_name}/commits',
44 44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 46 'pullrequest_show_all': '/{repo_name}/pull-request',
47 47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 50 'pullrequest_new': '/{repo_name}/pull-request/new',
51 51 'pullrequest_create': '/{repo_name}/pull-request/create',
52 52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 57 }[name].format(**kwargs)
58 58
59 59 if params:
60 60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
61 61 return base_url
62 62
63 63
64 64 @pytest.mark.usefixtures('app', 'autologin_user')
65 65 @pytest.mark.backends("git", "hg")
66 66 class TestPullrequestsView(object):
67 67
68 68 def test_index(self, backend):
69 69 self.app.get(route_path(
70 70 'pullrequest_new',
71 71 repo_name=backend.repo_name))
72 72
73 73 def test_option_menu_create_pull_request_exists(self, backend):
74 74 repo_name = backend.repo_name
75 75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
76 76
77 77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
78 78 'pullrequest_new', repo_name=repo_name)
79 79 response.mustcontain(create_pr_link)
80 80
81 81 def test_create_pr_form_with_raw_commit_id(self, backend):
82 82 repo = backend.repo
83 83
84 84 self.app.get(
85 85 route_path('pullrequest_new', repo_name=repo.repo_name,
86 86 commit=repo.get_commit().raw_id),
87 87 status=200)
88 88
89 89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 90 @pytest.mark.parametrize('range_diff', ["0", "1"])
91 91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
92 92 pull_request = pr_util.create_pull_request(
93 93 mergeable=pr_merge_enabled, enable_notifications=False)
94 94
95 95 response = self.app.get(route_path(
96 96 'pullrequest_show',
97 97 repo_name=pull_request.target_repo.scm_instance().name,
98 98 pull_request_id=pull_request.pull_request_id,
99 99 params={'range-diff': range_diff}))
100 100
101 101 for commit_id in pull_request.revisions:
102 102 response.mustcontain(commit_id)
103 103
104 assert pull_request.target_ref_parts.type in response
105 assert pull_request.target_ref_parts.name in response
106 target_clone_url = pull_request.target_repo.clone_url()
107 assert target_clone_url in response
104 response.mustcontain(pull_request.target_ref_parts.type)
105 response.mustcontain(pull_request.target_ref_parts.name)
108 106
109 assert 'class="pull-request-merge"' in response
107 response.mustcontain('class="pull-request-merge"')
108
110 109 if pr_merge_enabled:
111 110 response.mustcontain('Pull request reviewer approval is pending')
112 111 else:
113 112 response.mustcontain('Server-side pull request merging is disabled.')
114 113
115 114 if range_diff == "1":
116 115 response.mustcontain('Turn off: Show the diff as commit range')
117 116
118 117 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
119 118 # Logout
120 119 response = self.app.post(
121 120 h.route_path('logout'),
122 121 params={'csrf_token': csrf_token})
123 122 # Login as regular user
124 123 response = self.app.post(h.route_path('login'),
125 124 {'username': TEST_USER_REGULAR_LOGIN,
126 125 'password': 'test12'})
127 126
128 127 pull_request = pr_util.create_pull_request(
129 128 author=TEST_USER_REGULAR_LOGIN)
130 129
131 130 response = self.app.get(route_path(
132 131 'pullrequest_show',
133 132 repo_name=pull_request.target_repo.scm_instance().name,
134 133 pull_request_id=pull_request.pull_request_id))
135 134
136 135 response.mustcontain('Server-side pull request merging is disabled.')
137 136
138 137 assert_response = response.assert_response()
139 138 # for regular user without a merge permissions, we don't see it
140 139 assert_response.no_element_exists('#close-pull-request-action')
141 140
142 141 user_util.grant_user_permission_to_repo(
143 142 pull_request.target_repo,
144 143 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
145 144 'repository.write')
146 145 response = self.app.get(route_path(
147 146 'pullrequest_show',
148 147 repo_name=pull_request.target_repo.scm_instance().name,
149 148 pull_request_id=pull_request.pull_request_id))
150 149
151 150 response.mustcontain('Server-side pull request merging is disabled.')
152 151
153 152 assert_response = response.assert_response()
154 153 # now regular user has a merge permissions, we have CLOSE button
155 154 assert_response.one_element_exists('#close-pull-request-action')
156 155
157 156 def test_show_invalid_commit_id(self, pr_util):
158 157 # Simulating invalid revisions which will cause a lookup error
159 158 pull_request = pr_util.create_pull_request()
160 159 pull_request.revisions = ['invalid']
161 160 Session().add(pull_request)
162 161 Session().commit()
163 162
164 163 response = self.app.get(route_path(
165 164 'pullrequest_show',
166 165 repo_name=pull_request.target_repo.scm_instance().name,
167 166 pull_request_id=pull_request.pull_request_id))
168 167
169 168 for commit_id in pull_request.revisions:
170 169 response.mustcontain(commit_id)
171 170
172 171 def test_show_invalid_source_reference(self, pr_util):
173 172 pull_request = pr_util.create_pull_request()
174 173 pull_request.source_ref = 'branch:b:invalid'
175 174 Session().add(pull_request)
176 175 Session().commit()
177 176
178 177 self.app.get(route_path(
179 178 'pullrequest_show',
180 179 repo_name=pull_request.target_repo.scm_instance().name,
181 180 pull_request_id=pull_request.pull_request_id))
182 181
183 182 def test_edit_title_description(self, pr_util, csrf_token):
184 183 pull_request = pr_util.create_pull_request()
185 184 pull_request_id = pull_request.pull_request_id
186 185
187 186 response = self.app.post(
188 187 route_path('pullrequest_update',
189 188 repo_name=pull_request.target_repo.repo_name,
190 189 pull_request_id=pull_request_id),
191 190 params={
192 191 'edit_pull_request': 'true',
193 192 'title': 'New title',
194 193 'description': 'New description',
195 194 'csrf_token': csrf_token})
196 195
197 196 assert_session_flash(
198 197 response, u'Pull request title & description updated.',
199 198 category='success')
200 199
201 200 pull_request = PullRequest.get(pull_request_id)
202 201 assert pull_request.title == 'New title'
203 202 assert pull_request.description == 'New description'
204 203
205 204 def test_edit_title_description_closed(self, pr_util, csrf_token):
206 205 pull_request = pr_util.create_pull_request()
207 206 pull_request_id = pull_request.pull_request_id
208 207 repo_name = pull_request.target_repo.repo_name
209 208 pr_util.close()
210 209
211 210 response = self.app.post(
212 211 route_path('pullrequest_update',
213 212 repo_name=repo_name, pull_request_id=pull_request_id),
214 213 params={
215 214 'edit_pull_request': 'true',
216 215 'title': 'New title',
217 216 'description': 'New description',
218 217 'csrf_token': csrf_token}, status=200)
219 218 assert_session_flash(
220 219 response, u'Cannot update closed pull requests.',
221 220 category='error')
222 221
223 222 def test_update_invalid_source_reference(self, pr_util, csrf_token):
224 223 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
225 224
226 225 pull_request = pr_util.create_pull_request()
227 226 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
228 227 Session().add(pull_request)
229 228 Session().commit()
230 229
231 230 pull_request_id = pull_request.pull_request_id
232 231
233 232 response = self.app.post(
234 233 route_path('pullrequest_update',
235 234 repo_name=pull_request.target_repo.repo_name,
236 235 pull_request_id=pull_request_id),
237 236 params={'update_commits': 'true', 'csrf_token': csrf_token})
238 237
239 238 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 239 UpdateFailureReason.MISSING_SOURCE_REF])
241 240 assert_session_flash(response, expected_msg, category='error')
242 241
243 242 def test_missing_target_reference(self, pr_util, csrf_token):
244 243 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 244 pull_request = pr_util.create_pull_request(
246 245 approved=True, mergeable=True)
247 246 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
248 247 pull_request.target_ref = unicode_reference
249 248 Session().add(pull_request)
250 249 Session().commit()
251 250
252 251 pull_request_id = pull_request.pull_request_id
253 252 pull_request_url = route_path(
254 253 'pullrequest_show',
255 254 repo_name=pull_request.target_repo.repo_name,
256 255 pull_request_id=pull_request_id)
257 256
258 257 response = self.app.get(pull_request_url)
259 258 target_ref_id = 'invalid-branch'
260 259 merge_resp = MergeResponse(
261 260 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
262 261 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
263 262 response.assert_response().element_contains(
264 263 'div[data-role="merge-message"]', merge_resp.merge_status_message)
265 264
266 265 def test_comment_and_close_pull_request_custom_message_approved(
267 266 self, pr_util, csrf_token, xhr_header):
268 267
269 268 pull_request = pr_util.create_pull_request(approved=True)
270 269 pull_request_id = pull_request.pull_request_id
271 270 author = pull_request.user_id
272 271 repo = pull_request.target_repo.repo_id
273 272
274 273 self.app.post(
275 274 route_path('pullrequest_comment_create',
276 275 repo_name=pull_request.target_repo.scm_instance().name,
277 276 pull_request_id=pull_request_id),
278 277 params={
279 278 'close_pull_request': '1',
280 279 'text': 'Closing a PR',
281 280 'csrf_token': csrf_token},
282 281 extra_environ=xhr_header,)
283 282
284 283 journal = UserLog.query()\
285 284 .filter(UserLog.user_id == author)\
286 285 .filter(UserLog.repository_id == repo) \
287 286 .order_by(UserLog.user_log_id.asc()) \
288 287 .all()
289 288 assert journal[-1].action == 'repo.pull_request.close'
290 289
291 290 pull_request = PullRequest.get(pull_request_id)
292 291 assert pull_request.is_closed()
293 292
294 293 status = ChangesetStatusModel().get_status(
295 294 pull_request.source_repo, pull_request=pull_request)
296 295 assert status == ChangesetStatus.STATUS_APPROVED
297 296 comments = ChangesetComment().query() \
298 297 .filter(ChangesetComment.pull_request == pull_request) \
299 298 .order_by(ChangesetComment.comment_id.asc())\
300 299 .all()
301 300 assert comments[-1].text == 'Closing a PR'
302 301
303 302 def test_comment_force_close_pull_request_rejected(
304 303 self, pr_util, csrf_token, xhr_header):
305 304 pull_request = pr_util.create_pull_request()
306 305 pull_request_id = pull_request.pull_request_id
307 306 PullRequestModel().update_reviewers(
308 307 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
309 308 pull_request.author)
310 309 author = pull_request.user_id
311 310 repo = pull_request.target_repo.repo_id
312 311
313 312 self.app.post(
314 313 route_path('pullrequest_comment_create',
315 314 repo_name=pull_request.target_repo.scm_instance().name,
316 315 pull_request_id=pull_request_id),
317 316 params={
318 317 'close_pull_request': '1',
319 318 'csrf_token': csrf_token},
320 319 extra_environ=xhr_header)
321 320
322 321 pull_request = PullRequest.get(pull_request_id)
323 322
324 323 journal = UserLog.query()\
325 324 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
326 325 .order_by(UserLog.user_log_id.asc()) \
327 326 .all()
328 327 assert journal[-1].action == 'repo.pull_request.close'
329 328
330 329 # check only the latest status, not the review status
331 330 status = ChangesetStatusModel().get_status(
332 331 pull_request.source_repo, pull_request=pull_request)
333 332 assert status == ChangesetStatus.STATUS_REJECTED
334 333
335 334 def test_comment_and_close_pull_request(
336 335 self, pr_util, csrf_token, xhr_header):
337 336 pull_request = pr_util.create_pull_request()
338 337 pull_request_id = pull_request.pull_request_id
339 338
340 339 response = self.app.post(
341 340 route_path('pullrequest_comment_create',
342 341 repo_name=pull_request.target_repo.scm_instance().name,
343 342 pull_request_id=pull_request.pull_request_id),
344 343 params={
345 344 'close_pull_request': 'true',
346 345 'csrf_token': csrf_token},
347 346 extra_environ=xhr_header)
348 347
349 348 assert response.json
350 349
351 350 pull_request = PullRequest.get(pull_request_id)
352 351 assert pull_request.is_closed()
353 352
354 353 # check only the latest status, not the review status
355 354 status = ChangesetStatusModel().get_status(
356 355 pull_request.source_repo, pull_request=pull_request)
357 356 assert status == ChangesetStatus.STATUS_REJECTED
358 357
359 358 def test_create_pull_request(self, backend, csrf_token):
360 359 commits = [
361 360 {'message': 'ancestor'},
362 361 {'message': 'change'},
363 362 {'message': 'change2'},
364 363 ]
365 364 commit_ids = backend.create_master_repo(commits)
366 365 target = backend.create_repo(heads=['ancestor'])
367 366 source = backend.create_repo(heads=['change2'])
368 367
369 368 response = self.app.post(
370 369 route_path('pullrequest_create', repo_name=source.repo_name),
371 370 [
372 371 ('source_repo', source.repo_name),
373 372 ('source_ref', 'branch:default:' + commit_ids['change2']),
374 373 ('target_repo', target.repo_name),
375 374 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
376 375 ('common_ancestor', commit_ids['ancestor']),
377 376 ('pullrequest_title', 'Title'),
378 377 ('pullrequest_desc', 'Description'),
379 378 ('description_renderer', 'markdown'),
380 379 ('__start__', 'review_members:sequence'),
381 380 ('__start__', 'reviewer:mapping'),
382 381 ('user_id', '1'),
383 382 ('__start__', 'reasons:sequence'),
384 383 ('reason', 'Some reason'),
385 384 ('__end__', 'reasons:sequence'),
386 385 ('__start__', 'rules:sequence'),
387 386 ('__end__', 'rules:sequence'),
388 387 ('mandatory', 'False'),
389 388 ('__end__', 'reviewer:mapping'),
390 389 ('__end__', 'review_members:sequence'),
391 390 ('__start__', 'revisions:sequence'),
392 391 ('revisions', commit_ids['change']),
393 392 ('revisions', commit_ids['change2']),
394 393 ('__end__', 'revisions:sequence'),
395 394 ('user', ''),
396 395 ('csrf_token', csrf_token),
397 396 ],
398 397 status=302)
399 398
400 399 location = response.headers['Location']
401 400 pull_request_id = location.rsplit('/', 1)[1]
402 401 assert pull_request_id != 'new'
403 402 pull_request = PullRequest.get(int(pull_request_id))
404 403
405 404 # check that we have now both revisions
406 405 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
407 406 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
408 407 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
409 408 assert pull_request.target_ref == expected_target_ref
410 409
411 410 def test_reviewer_notifications(self, backend, csrf_token):
412 411 # We have to use the app.post for this test so it will create the
413 412 # notifications properly with the new PR
414 413 commits = [
415 414 {'message': 'ancestor',
416 415 'added': [FileNode('file_A', content='content_of_ancestor')]},
417 416 {'message': 'change',
418 417 'added': [FileNode('file_a', content='content_of_change')]},
419 418 {'message': 'change-child'},
420 419 {'message': 'ancestor-child', 'parents': ['ancestor'],
421 420 'added': [
422 421 FileNode('file_B', content='content_of_ancestor_child')]},
423 422 {'message': 'ancestor-child-2'},
424 423 ]
425 424 commit_ids = backend.create_master_repo(commits)
426 425 target = backend.create_repo(heads=['ancestor-child'])
427 426 source = backend.create_repo(heads=['change'])
428 427
429 428 response = self.app.post(
430 429 route_path('pullrequest_create', repo_name=source.repo_name),
431 430 [
432 431 ('source_repo', source.repo_name),
433 432 ('source_ref', 'branch:default:' + commit_ids['change']),
434 433 ('target_repo', target.repo_name),
435 434 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
436 435 ('common_ancestor', commit_ids['ancestor']),
437 436 ('pullrequest_title', 'Title'),
438 437 ('pullrequest_desc', 'Description'),
439 438 ('description_renderer', 'markdown'),
440 439 ('__start__', 'review_members:sequence'),
441 440 ('__start__', 'reviewer:mapping'),
442 441 ('user_id', '2'),
443 442 ('__start__', 'reasons:sequence'),
444 443 ('reason', 'Some reason'),
445 444 ('__end__', 'reasons:sequence'),
446 445 ('__start__', 'rules:sequence'),
447 446 ('__end__', 'rules:sequence'),
448 447 ('mandatory', 'False'),
449 448 ('__end__', 'reviewer:mapping'),
450 449 ('__end__', 'review_members:sequence'),
451 450 ('__start__', 'revisions:sequence'),
452 451 ('revisions', commit_ids['change']),
453 452 ('__end__', 'revisions:sequence'),
454 453 ('user', ''),
455 454 ('csrf_token', csrf_token),
456 455 ],
457 456 status=302)
458 457
459 458 location = response.headers['Location']
460 459
461 460 pull_request_id = location.rsplit('/', 1)[1]
462 461 assert pull_request_id != 'new'
463 462 pull_request = PullRequest.get(int(pull_request_id))
464 463
465 464 # Check that a notification was made
466 465 notifications = Notification.query()\
467 466 .filter(Notification.created_by == pull_request.author.user_id,
468 467 Notification.type_ == Notification.TYPE_PULL_REQUEST,
469 468 Notification.subject.contains(
470 469 "requested a pull request review. !%s" % pull_request_id))
471 470 assert len(notifications.all()) == 1
472 471
473 472 # Change reviewers and check that a notification was made
474 473 PullRequestModel().update_reviewers(
475 474 pull_request.pull_request_id, [(1, [], False, [])],
476 475 pull_request.author)
477 476 assert len(notifications.all()) == 2
478 477
479 478 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
480 479 csrf_token):
481 480 commits = [
482 481 {'message': 'ancestor',
483 482 'added': [FileNode('file_A', content='content_of_ancestor')]},
484 483 {'message': 'change',
485 484 'added': [FileNode('file_a', content='content_of_change')]},
486 485 {'message': 'change-child'},
487 486 {'message': 'ancestor-child', 'parents': ['ancestor'],
488 487 'added': [
489 488 FileNode('file_B', content='content_of_ancestor_child')]},
490 489 {'message': 'ancestor-child-2'},
491 490 ]
492 491 commit_ids = backend.create_master_repo(commits)
493 492 target = backend.create_repo(heads=['ancestor-child'])
494 493 source = backend.create_repo(heads=['change'])
495 494
496 495 response = self.app.post(
497 496 route_path('pullrequest_create', repo_name=source.repo_name),
498 497 [
499 498 ('source_repo', source.repo_name),
500 499 ('source_ref', 'branch:default:' + commit_ids['change']),
501 500 ('target_repo', target.repo_name),
502 501 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
503 502 ('common_ancestor', commit_ids['ancestor']),
504 503 ('pullrequest_title', 'Title'),
505 504 ('pullrequest_desc', 'Description'),
506 505 ('description_renderer', 'markdown'),
507 506 ('__start__', 'review_members:sequence'),
508 507 ('__start__', 'reviewer:mapping'),
509 508 ('user_id', '1'),
510 509 ('__start__', 'reasons:sequence'),
511 510 ('reason', 'Some reason'),
512 511 ('__end__', 'reasons:sequence'),
513 512 ('__start__', 'rules:sequence'),
514 513 ('__end__', 'rules:sequence'),
515 514 ('mandatory', 'False'),
516 515 ('__end__', 'reviewer:mapping'),
517 516 ('__end__', 'review_members:sequence'),
518 517 ('__start__', 'revisions:sequence'),
519 518 ('revisions', commit_ids['change']),
520 519 ('__end__', 'revisions:sequence'),
521 520 ('user', ''),
522 521 ('csrf_token', csrf_token),
523 522 ],
524 523 status=302)
525 524
526 525 location = response.headers['Location']
527 526
528 527 pull_request_id = location.rsplit('/', 1)[1]
529 528 assert pull_request_id != 'new'
530 529 pull_request = PullRequest.get(int(pull_request_id))
531 530
532 531 # target_ref has to point to the ancestor's commit_id in order to
533 532 # show the correct diff
534 533 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
535 534 assert pull_request.target_ref == expected_target_ref
536 535
537 536 # Check generated diff contents
538 537 response = response.follow()
539 assert 'content_of_ancestor' not in response.body
540 assert 'content_of_ancestor-child' not in response.body
541 assert 'content_of_change' in response.body
538 response.mustcontain(no=['content_of_ancestor'])
539 response.mustcontain(no=['content_of_ancestor-child'])
540 response.mustcontain('content_of_change')
542 541
543 542 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
544 543 # Clear any previous calls to rcextensions
545 544 rhodecode.EXTENSIONS.calls.clear()
546 545
547 546 pull_request = pr_util.create_pull_request(
548 547 approved=True, mergeable=True)
549 548 pull_request_id = pull_request.pull_request_id
550 549 repo_name = pull_request.target_repo.scm_instance().name,
551 550
552 551 url = route_path('pullrequest_merge',
553 552 repo_name=str(repo_name[0]),
554 553 pull_request_id=pull_request_id)
555 554 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
556 555
557 556 pull_request = PullRequest.get(pull_request_id)
558 557
559 558 assert response.status_int == 200
560 559 assert pull_request.is_closed()
561 560 assert_pull_request_status(
562 561 pull_request, ChangesetStatus.STATUS_APPROVED)
563 562
564 563 # Check the relevant log entries were added
565 564 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
566 565 actions = [log.action for log in user_logs]
567 566 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
568 567 expected_actions = [
569 568 u'repo.pull_request.close',
570 569 u'repo.pull_request.merge',
571 570 u'repo.pull_request.comment.create'
572 571 ]
573 572 assert actions == expected_actions
574 573
575 574 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
576 575 actions = [log for log in user_logs]
577 576 assert actions[-1].action == 'user.push'
578 577 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
579 578
580 579 # Check post_push rcextension was really executed
581 580 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
582 581 assert len(push_calls) == 1
583 582 unused_last_call_args, last_call_kwargs = push_calls[0]
584 583 assert last_call_kwargs['action'] == 'push'
585 584 assert last_call_kwargs['commit_ids'] == pr_commit_ids
586 585
587 586 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
588 587 pull_request = pr_util.create_pull_request(mergeable=False)
589 588 pull_request_id = pull_request.pull_request_id
590 589 pull_request = PullRequest.get(pull_request_id)
591 590
592 591 response = self.app.post(
593 592 route_path('pullrequest_merge',
594 593 repo_name=pull_request.target_repo.scm_instance().name,
595 594 pull_request_id=pull_request.pull_request_id),
596 595 params={'csrf_token': csrf_token}).follow()
597 596
598 597 assert response.status_int == 200
599 598 response.mustcontain(
600 599 'Merge is not currently possible because of below failed checks.')
601 600 response.mustcontain('Server-side pull request merging is disabled.')
602 601
603 602 @pytest.mark.skip_backends('svn')
604 603 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
605 604 pull_request = pr_util.create_pull_request(mergeable=True)
606 605 pull_request_id = pull_request.pull_request_id
607 606 repo_name = pull_request.target_repo.scm_instance().name
608 607
609 608 response = self.app.post(
610 609 route_path('pullrequest_merge',
611 610 repo_name=repo_name, pull_request_id=pull_request_id),
612 611 params={'csrf_token': csrf_token}).follow()
613 612
614 613 assert response.status_int == 200
615 614
616 615 response.mustcontain(
617 616 'Merge is not currently possible because of below failed checks.')
618 617 response.mustcontain('Pull request reviewer approval is pending.')
619 618
620 619 def test_merge_pull_request_renders_failure_reason(
621 620 self, user_regular, csrf_token, pr_util):
622 621 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
623 622 pull_request_id = pull_request.pull_request_id
624 623 repo_name = pull_request.target_repo.scm_instance().name
625 624
626 625 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
627 626 MergeFailureReason.PUSH_FAILED,
628 627 metadata={'target': 'shadow repo',
629 628 'merge_commit': 'xxx'})
630 629 model_patcher = mock.patch.multiple(
631 630 PullRequestModel,
632 631 merge_repo=mock.Mock(return_value=merge_resp),
633 632 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
634 633
635 634 with model_patcher:
636 635 response = self.app.post(
637 636 route_path('pullrequest_merge',
638 637 repo_name=repo_name,
639 638 pull_request_id=pull_request_id),
640 639 params={'csrf_token': csrf_token}, status=302)
641 640
642 641 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
643 642 metadata={'target': 'shadow repo',
644 643 'merge_commit': 'xxx'})
645 644 assert_session_flash(response, merge_resp.merge_status_message)
646 645
647 646 def test_update_source_revision(self, backend, csrf_token):
648 647 commits = [
649 648 {'message': 'ancestor'},
650 649 {'message': 'change'},
651 650 {'message': 'change-2'},
652 651 ]
653 652 commit_ids = backend.create_master_repo(commits)
654 653 target = backend.create_repo(heads=['ancestor'])
655 654 source = backend.create_repo(heads=['change'])
656 655
657 656 # create pr from a in source to A in target
658 657 pull_request = PullRequest()
659 658
660 659 pull_request.source_repo = source
661 660 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
662 661 branch=backend.default_branch_name, commit_id=commit_ids['change'])
663 662
664 663 pull_request.target_repo = target
665 664 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
666 665 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
667 666
668 667 pull_request.revisions = [commit_ids['change']]
669 668 pull_request.title = u"Test"
670 669 pull_request.description = u"Description"
671 670 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
672 671 pull_request.pull_request_state = PullRequest.STATE_CREATED
673 672 Session().add(pull_request)
674 673 Session().commit()
675 674 pull_request_id = pull_request.pull_request_id
676 675
677 676 # source has ancestor - change - change-2
678 677 backend.pull_heads(source, heads=['change-2'])
679 678
680 679 # update PR
681 680 self.app.post(
682 681 route_path('pullrequest_update',
683 682 repo_name=target.repo_name, pull_request_id=pull_request_id),
684 683 params={'update_commits': 'true', 'csrf_token': csrf_token})
685 684
686 685 response = self.app.get(
687 686 route_path('pullrequest_show',
688 687 repo_name=target.repo_name,
689 688 pull_request_id=pull_request.pull_request_id))
690 689
691 690 assert response.status_int == 200
692 assert 'Pull request updated to' in response.body
693 assert 'with 1 added, 0 removed commits.' in response.body
691 response.mustcontain('Pull request updated to')
692 response.mustcontain('with 1 added, 0 removed commits.')
694 693
695 694 # check that we have now both revisions
696 695 pull_request = PullRequest.get(pull_request_id)
697 696 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
698 697
699 698 def test_update_target_revision(self, backend, csrf_token):
700 699 commits = [
701 700 {'message': 'ancestor'},
702 701 {'message': 'change'},
703 702 {'message': 'ancestor-new', 'parents': ['ancestor']},
704 703 {'message': 'change-rebased'},
705 704 ]
706 705 commit_ids = backend.create_master_repo(commits)
707 706 target = backend.create_repo(heads=['ancestor'])
708 707 source = backend.create_repo(heads=['change'])
709 708
710 709 # create pr from a in source to A in target
711 710 pull_request = PullRequest()
712 711
713 712 pull_request.source_repo = source
714 713 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
715 714 branch=backend.default_branch_name, commit_id=commit_ids['change'])
716 715
717 716 pull_request.target_repo = target
718 717 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
719 718 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
720 719
721 720 pull_request.revisions = [commit_ids['change']]
722 721 pull_request.title = u"Test"
723 722 pull_request.description = u"Description"
724 723 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
725 724 pull_request.pull_request_state = PullRequest.STATE_CREATED
726 725
727 726 Session().add(pull_request)
728 727 Session().commit()
729 728 pull_request_id = pull_request.pull_request_id
730 729
731 730 # target has ancestor - ancestor-new
732 731 # source has ancestor - ancestor-new - change-rebased
733 732 backend.pull_heads(target, heads=['ancestor-new'])
734 733 backend.pull_heads(source, heads=['change-rebased'])
735 734
736 735 # update PR
737 736 url = route_path('pullrequest_update',
738 737 repo_name=target.repo_name,
739 738 pull_request_id=pull_request_id)
740 739 self.app.post(url,
741 740 params={'update_commits': 'true', 'csrf_token': csrf_token},
742 741 status=200)
743 742
744 743 # check that we have now both revisions
745 744 pull_request = PullRequest.get(pull_request_id)
746 745 assert pull_request.revisions == [commit_ids['change-rebased']]
747 746 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
748 747 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
749 748
750 749 response = self.app.get(
751 750 route_path('pullrequest_show',
752 751 repo_name=target.repo_name,
753 752 pull_request_id=pull_request.pull_request_id))
754 753 assert response.status_int == 200
755 assert 'Pull request updated to' in response.body
756 assert 'with 1 added, 1 removed commits.' in response.body
754 response.mustcontain('Pull request updated to')
755 response.mustcontain('with 1 added, 1 removed commits.')
757 756
758 757 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
759 758 backend = backend_git
760 759 commits = [
761 760 {'message': 'master-commit-1'},
762 761 {'message': 'master-commit-2-change-1'},
763 762 {'message': 'master-commit-3-change-2'},
764 763
765 764 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
766 765 {'message': 'feat-commit-2'},
767 766 ]
768 767 commit_ids = backend.create_master_repo(commits)
769 768 target = backend.create_repo(heads=['master-commit-3-change-2'])
770 769 source = backend.create_repo(heads=['feat-commit-2'])
771 770
772 771 # create pr from a in source to A in target
773 772 pull_request = PullRequest()
774 773 pull_request.source_repo = source
775 774
776 775 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
777 776 branch=backend.default_branch_name,
778 777 commit_id=commit_ids['master-commit-3-change-2'])
779 778
780 779 pull_request.target_repo = target
781 780 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
782 781 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
783 782
784 783 pull_request.revisions = [
785 784 commit_ids['feat-commit-1'],
786 785 commit_ids['feat-commit-2']
787 786 ]
788 787 pull_request.title = u"Test"
789 788 pull_request.description = u"Description"
790 789 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
791 790 pull_request.pull_request_state = PullRequest.STATE_CREATED
792 791 Session().add(pull_request)
793 792 Session().commit()
794 793 pull_request_id = pull_request.pull_request_id
795 794
796 795 # PR is created, now we simulate a force-push into target,
797 796 # that drops a 2 last commits
798 797 vcsrepo = target.scm_instance()
799 798 vcsrepo.config.clear_section('hooks')
800 799 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
801 800
802 801 # update PR
803 802 url = route_path('pullrequest_update',
804 803 repo_name=target.repo_name,
805 804 pull_request_id=pull_request_id)
806 805 self.app.post(url,
807 806 params={'update_commits': 'true', 'csrf_token': csrf_token},
808 807 status=200)
809 808
810 809 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
811 810 assert response.status_int == 200
812 811 response.mustcontain('Pull request updated to')
813 812 response.mustcontain('with 0 added, 0 removed commits.')
814 813
815 814 def test_update_of_ancestor_reference(self, backend, csrf_token):
816 815 commits = [
817 816 {'message': 'ancestor'},
818 817 {'message': 'change'},
819 818 {'message': 'change-2'},
820 819 {'message': 'ancestor-new', 'parents': ['ancestor']},
821 820 {'message': 'change-rebased'},
822 821 ]
823 822 commit_ids = backend.create_master_repo(commits)
824 823 target = backend.create_repo(heads=['ancestor'])
825 824 source = backend.create_repo(heads=['change'])
826 825
827 826 # create pr from a in source to A in target
828 827 pull_request = PullRequest()
829 828 pull_request.source_repo = source
830 829
831 830 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
832 831 branch=backend.default_branch_name, commit_id=commit_ids['change'])
833 832 pull_request.target_repo = target
834 833 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
835 834 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
836 835 pull_request.revisions = [commit_ids['change']]
837 836 pull_request.title = u"Test"
838 837 pull_request.description = u"Description"
839 838 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
840 839 pull_request.pull_request_state = PullRequest.STATE_CREATED
841 840 Session().add(pull_request)
842 841 Session().commit()
843 842 pull_request_id = pull_request.pull_request_id
844 843
845 844 # target has ancestor - ancestor-new
846 845 # source has ancestor - ancestor-new - change-rebased
847 846 backend.pull_heads(target, heads=['ancestor-new'])
848 847 backend.pull_heads(source, heads=['change-rebased'])
849 848
850 849 # update PR
851 850 self.app.post(
852 851 route_path('pullrequest_update',
853 852 repo_name=target.repo_name, pull_request_id=pull_request_id),
854 853 params={'update_commits': 'true', 'csrf_token': csrf_token},
855 854 status=200)
856 855
857 856 # Expect the target reference to be updated correctly
858 857 pull_request = PullRequest.get(pull_request_id)
859 858 assert pull_request.revisions == [commit_ids['change-rebased']]
860 859 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
861 860 branch=backend.default_branch_name,
862 861 commit_id=commit_ids['ancestor-new'])
863 862 assert pull_request.target_ref == expected_target_ref
864 863
865 864 def test_remove_pull_request_branch(self, backend_git, csrf_token):
866 865 branch_name = 'development'
867 866 commits = [
868 867 {'message': 'initial-commit'},
869 868 {'message': 'old-feature'},
870 869 {'message': 'new-feature', 'branch': branch_name},
871 870 ]
872 871 repo = backend_git.create_repo(commits)
873 872 repo_name = repo.repo_name
874 873 commit_ids = backend_git.commit_ids
875 874
876 875 pull_request = PullRequest()
877 876 pull_request.source_repo = repo
878 877 pull_request.target_repo = repo
879 878 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
880 879 branch=branch_name, commit_id=commit_ids['new-feature'])
881 880 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
882 881 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
883 882 pull_request.revisions = [commit_ids['new-feature']]
884 883 pull_request.title = u"Test"
885 884 pull_request.description = u"Description"
886 885 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
887 886 pull_request.pull_request_state = PullRequest.STATE_CREATED
888 887 Session().add(pull_request)
889 888 Session().commit()
890 889
891 890 pull_request_id = pull_request.pull_request_id
892 891
893 892 vcs = repo.scm_instance()
894 893 vcs.remove_ref('refs/heads/{}'.format(branch_name))
895 894
896 895 response = self.app.get(route_path(
897 896 'pullrequest_show',
898 897 repo_name=repo_name,
899 898 pull_request_id=pull_request_id))
900 899
901 900 assert response.status_int == 200
902 901
903 902 response.assert_response().element_contains(
904 903 '#changeset_compare_view_content .alert strong',
905 904 'Missing commits')
906 905 response.assert_response().element_contains(
907 906 '#changeset_compare_view_content .alert',
908 907 'This pull request cannot be displayed, because one or more'
909 908 ' commits no longer exist in the source repository.')
910 909
911 910 def test_strip_commits_from_pull_request(
912 911 self, backend, pr_util, csrf_token):
913 912 commits = [
914 913 {'message': 'initial-commit'},
915 914 {'message': 'old-feature'},
916 915 {'message': 'new-feature', 'parents': ['initial-commit']},
917 916 ]
918 917 pull_request = pr_util.create_pull_request(
919 918 commits, target_head='initial-commit', source_head='new-feature',
920 919 revisions=['new-feature'])
921 920
922 921 vcs = pr_util.source_repository.scm_instance()
923 922 if backend.alias == 'git':
924 923 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
925 924 else:
926 925 vcs.strip(pr_util.commit_ids['new-feature'])
927 926
928 927 response = self.app.get(route_path(
929 928 'pullrequest_show',
930 929 repo_name=pr_util.target_repository.repo_name,
931 930 pull_request_id=pull_request.pull_request_id))
932 931
933 932 assert response.status_int == 200
934 933
935 934 response.assert_response().element_contains(
936 935 '#changeset_compare_view_content .alert strong',
937 936 'Missing commits')
938 937 response.assert_response().element_contains(
939 938 '#changeset_compare_view_content .alert',
940 939 'This pull request cannot be displayed, because one or more'
941 940 ' commits no longer exist in the source repository.')
942 941 response.assert_response().element_contains(
943 942 '#update_commits',
944 943 'Update commits')
945 944
946 945 def test_strip_commits_and_update(
947 946 self, backend, pr_util, csrf_token):
948 947 commits = [
949 948 {'message': 'initial-commit'},
950 949 {'message': 'old-feature'},
951 950 {'message': 'new-feature', 'parents': ['old-feature']},
952 951 ]
953 952 pull_request = pr_util.create_pull_request(
954 953 commits, target_head='old-feature', source_head='new-feature',
955 954 revisions=['new-feature'], mergeable=True)
956 955
957 956 vcs = pr_util.source_repository.scm_instance()
958 957 if backend.alias == 'git':
959 958 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
960 959 else:
961 960 vcs.strip(pr_util.commit_ids['new-feature'])
962 961
963 962 url = route_path('pullrequest_update',
964 963 repo_name=pull_request.target_repo.repo_name,
965 964 pull_request_id=pull_request.pull_request_id)
966 965 response = self.app.post(url,
967 966 params={'update_commits': 'true',
968 967 'csrf_token': csrf_token})
969 968
970 969 assert response.status_int == 200
971 970 assert response.body == '{"response": true, "redirect_url": null}'
972 971
973 972 # Make sure that after update, it won't raise 500 errors
974 973 response = self.app.get(route_path(
975 974 'pullrequest_show',
976 975 repo_name=pr_util.target_repository.repo_name,
977 976 pull_request_id=pull_request.pull_request_id))
978 977
979 978 assert response.status_int == 200
980 979 response.assert_response().element_contains(
981 980 '#changeset_compare_view_content .alert strong',
982 981 'Missing commits')
983 982
984 983 def test_branch_is_a_link(self, pr_util):
985 984 pull_request = pr_util.create_pull_request()
986 985 pull_request.source_ref = 'branch:origin:1234567890abcdef'
987 986 pull_request.target_ref = 'branch:target:abcdef1234567890'
988 987 Session().add(pull_request)
989 988 Session().commit()
990 989
991 990 response = self.app.get(route_path(
992 991 'pullrequest_show',
993 992 repo_name=pull_request.target_repo.scm_instance().name,
994 993 pull_request_id=pull_request.pull_request_id))
995 994 assert response.status_int == 200
996 995
997 origin = response.assert_response().get_element('.pr-origininfo .tag')
998 origin_children = origin.getchildren()
999 assert len(origin_children) == 1
1000 target = response.assert_response().get_element('.pr-targetinfo .tag')
1001 target_children = target.getchildren()
1002 assert len(target_children) == 1
996 source = response.assert_response().get_element('.pr-source-info')
997 source_parent = source.getparent()
998 assert len(source_parent) == 1
999
1000 target = response.assert_response().get_element('.pr-target-info')
1001 target_parent = target.getparent()
1002 assert len(target_parent) == 1
1003 1003
1004 1004 expected_origin_link = route_path(
1005 1005 'repo_commits',
1006 1006 repo_name=pull_request.source_repo.scm_instance().name,
1007 1007 params=dict(branch='origin'))
1008 1008 expected_target_link = route_path(
1009 1009 'repo_commits',
1010 1010 repo_name=pull_request.target_repo.scm_instance().name,
1011 1011 params=dict(branch='target'))
1012 assert origin_children[0].attrib['href'] == expected_origin_link
1013 assert origin_children[0].text == 'branch: origin'
1014 assert target_children[0].attrib['href'] == expected_target_link
1015 assert target_children[0].text == 'branch: target'
1012 assert source_parent.attrib['href'] == expected_origin_link
1013 assert target_parent.attrib['href'] == expected_target_link
1016 1014
1017 1015 def test_bookmark_is_not_a_link(self, pr_util):
1018 1016 pull_request = pr_util.create_pull_request()
1019 1017 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1020 1018 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1021 1019 Session().add(pull_request)
1022 1020 Session().commit()
1023 1021
1024 1022 response = self.app.get(route_path(
1025 1023 'pullrequest_show',
1026 1024 repo_name=pull_request.target_repo.scm_instance().name,
1027 1025 pull_request_id=pull_request.pull_request_id))
1028 1026 assert response.status_int == 200
1029 1027
1030 origin = response.assert_response().get_element('.pr-origininfo .tag')
1031 assert origin.text.strip() == 'bookmark: origin'
1032 assert origin.getchildren() == []
1028 source = response.assert_response().get_element('.pr-source-info')
1029 assert source.text.strip() == 'bookmark:origin'
1030 assert source.getparent().attrib.get('href') is None
1033 1031
1034 target = response.assert_response().get_element('.pr-targetinfo .tag')
1035 assert target.text.strip() == 'bookmark: target'
1036 assert target.getchildren() == []
1032 target = response.assert_response().get_element('.pr-target-info')
1033 assert target.text.strip() == 'bookmark:target'
1034 assert target.getparent().attrib.get('href') is None
1037 1035
1038 1036 def test_tag_is_not_a_link(self, pr_util):
1039 1037 pull_request = pr_util.create_pull_request()
1040 1038 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1041 1039 pull_request.target_ref = 'tag:target:abcdef1234567890'
1042 1040 Session().add(pull_request)
1043 1041 Session().commit()
1044 1042
1045 1043 response = self.app.get(route_path(
1046 1044 'pullrequest_show',
1047 1045 repo_name=pull_request.target_repo.scm_instance().name,
1048 1046 pull_request_id=pull_request.pull_request_id))
1049 1047 assert response.status_int == 200
1050 1048
1051 origin = response.assert_response().get_element('.pr-origininfo .tag')
1052 assert origin.text.strip() == 'tag: origin'
1053 assert origin.getchildren() == []
1049 source = response.assert_response().get_element('.pr-source-info')
1050 assert source.text.strip() == 'tag:origin'
1051 assert source.getparent().attrib.get('href') is None
1054 1052
1055 target = response.assert_response().get_element('.pr-targetinfo .tag')
1056 assert target.text.strip() == 'tag: target'
1057 assert target.getchildren() == []
1053 target = response.assert_response().get_element('.pr-target-info')
1054 assert target.text.strip() == 'tag:target'
1055 assert target.getparent().attrib.get('href') is None
1058 1056
1059 1057 @pytest.mark.parametrize('mergeable', [True, False])
1060 1058 def test_shadow_repository_link(
1061 1059 self, mergeable, pr_util, http_host_only_stub):
1062 1060 """
1063 1061 Check that the pull request summary page displays a link to the shadow
1064 1062 repository if the pull request is mergeable. If it is not mergeable
1065 1063 the link should not be displayed.
1066 1064 """
1067 1065 pull_request = pr_util.create_pull_request(
1068 1066 mergeable=mergeable, enable_notifications=False)
1069 1067 target_repo = pull_request.target_repo.scm_instance()
1070 1068 pr_id = pull_request.pull_request_id
1071 1069 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1072 1070 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1073 1071
1074 1072 response = self.app.get(route_path(
1075 1073 'pullrequest_show',
1076 1074 repo_name=target_repo.name,
1077 1075 pull_request_id=pr_id))
1078 1076
1079 1077 if mergeable:
1080 1078 response.assert_response().element_value_contains(
1081 1079 'input.pr-mergeinfo', shadow_url)
1082 1080 response.assert_response().element_value_contains(
1083 1081 'input.pr-mergeinfo ', 'pr-merge')
1084 1082 else:
1085 1083 response.assert_response().no_element_exists('.pr-mergeinfo')
1086 1084
1087 1085
1088 1086 @pytest.mark.usefixtures('app')
1089 1087 @pytest.mark.backends("git", "hg")
1090 1088 class TestPullrequestsControllerDelete(object):
1091 1089 def test_pull_request_delete_button_permissions_admin(
1092 1090 self, autologin_user, user_admin, pr_util):
1093 1091 pull_request = pr_util.create_pull_request(
1094 1092 author=user_admin.username, enable_notifications=False)
1095 1093
1096 1094 response = self.app.get(route_path(
1097 1095 'pullrequest_show',
1098 1096 repo_name=pull_request.target_repo.scm_instance().name,
1099 1097 pull_request_id=pull_request.pull_request_id))
1100 1098
1101 1099 response.mustcontain('id="delete_pullrequest"')
1102 1100 response.mustcontain('Confirm to delete this pull request')
1103 1101
1104 1102 def test_pull_request_delete_button_permissions_owner(
1105 1103 self, autologin_regular_user, user_regular, pr_util):
1106 1104 pull_request = pr_util.create_pull_request(
1107 1105 author=user_regular.username, enable_notifications=False)
1108 1106
1109 1107 response = self.app.get(route_path(
1110 1108 'pullrequest_show',
1111 1109 repo_name=pull_request.target_repo.scm_instance().name,
1112 1110 pull_request_id=pull_request.pull_request_id))
1113 1111
1114 1112 response.mustcontain('id="delete_pullrequest"')
1115 1113 response.mustcontain('Confirm to delete this pull request')
1116 1114
1117 1115 def test_pull_request_delete_button_permissions_forbidden(
1118 1116 self, autologin_regular_user, user_regular, user_admin, pr_util):
1119 1117 pull_request = pr_util.create_pull_request(
1120 1118 author=user_admin.username, enable_notifications=False)
1121 1119
1122 1120 response = self.app.get(route_path(
1123 1121 'pullrequest_show',
1124 1122 repo_name=pull_request.target_repo.scm_instance().name,
1125 1123 pull_request_id=pull_request.pull_request_id))
1126 1124 response.mustcontain(no=['id="delete_pullrequest"'])
1127 1125 response.mustcontain(no=['Confirm to delete this pull request'])
1128 1126
1129 1127 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1130 1128 self, autologin_regular_user, user_regular, user_admin, pr_util,
1131 1129 user_util):
1132 1130
1133 1131 pull_request = pr_util.create_pull_request(
1134 1132 author=user_admin.username, enable_notifications=False)
1135 1133
1136 1134 user_util.grant_user_permission_to_repo(
1137 1135 pull_request.target_repo, user_regular,
1138 1136 'repository.write')
1139 1137
1140 1138 response = self.app.get(route_path(
1141 1139 'pullrequest_show',
1142 1140 repo_name=pull_request.target_repo.scm_instance().name,
1143 1141 pull_request_id=pull_request.pull_request_id))
1144 1142
1145 1143 response.mustcontain('id="open_edit_pullrequest"')
1146 1144 response.mustcontain('id="delete_pullrequest"')
1147 1145 response.mustcontain(no=['Confirm to delete this pull request'])
1148 1146
1149 1147 def test_delete_comment_returns_404_if_comment_does_not_exist(
1150 1148 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1151 1149
1152 1150 pull_request = pr_util.create_pull_request(
1153 1151 author=user_admin.username, enable_notifications=False)
1154 1152
1155 1153 self.app.post(
1156 1154 route_path(
1157 1155 'pullrequest_comment_delete',
1158 1156 repo_name=pull_request.target_repo.scm_instance().name,
1159 1157 pull_request_id=pull_request.pull_request_id,
1160 1158 comment_id=1024404),
1161 1159 extra_environ=xhr_header,
1162 1160 params={'csrf_token': csrf_token},
1163 1161 status=404
1164 1162 )
1165 1163
1166 1164 def test_delete_comment(
1167 1165 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1168 1166
1169 1167 pull_request = pr_util.create_pull_request(
1170 1168 author=user_admin.username, enable_notifications=False)
1171 1169 comment = pr_util.create_comment()
1172 1170 comment_id = comment.comment_id
1173 1171
1174 1172 response = self.app.post(
1175 1173 route_path(
1176 1174 'pullrequest_comment_delete',
1177 1175 repo_name=pull_request.target_repo.scm_instance().name,
1178 1176 pull_request_id=pull_request.pull_request_id,
1179 1177 comment_id=comment_id),
1180 1178 extra_environ=xhr_header,
1181 1179 params={'csrf_token': csrf_token},
1182 1180 status=200
1183 1181 )
1184 1182 assert response.body == 'true'
1185 1183
1186 1184 @pytest.mark.parametrize('url_type', [
1187 1185 'pullrequest_new',
1188 1186 'pullrequest_create',
1189 1187 'pullrequest_update',
1190 1188 'pullrequest_merge',
1191 1189 ])
1192 1190 def test_pull_request_is_forbidden_on_archived_repo(
1193 1191 self, autologin_user, backend, xhr_header, user_util, url_type):
1194 1192
1195 1193 # create a temporary repo
1196 1194 source = user_util.create_repo(repo_type=backend.alias)
1197 1195 repo_name = source.repo_name
1198 1196 repo = Repository.get_by_repo_name(repo_name)
1199 1197 repo.archived = True
1200 1198 Session().commit()
1201 1199
1202 1200 response = self.app.get(
1203 1201 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1204 1202
1205 1203 msg = 'Action not supported for archived repository.'
1206 1204 assert_session_flash(response, msg)
1207 1205
1208 1206
1209 1207 def assert_pull_request_status(pull_request, expected_status):
1210 1208 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1211 1209 assert status == expected_status
1212 1210
1213 1211
1214 1212 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1215 1213 @pytest.mark.usefixtures("autologin_user")
1216 1214 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1217 1215 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,1943 +1,1945 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import StringIO
32 32 import textwrap
33 33 import urllib
34 34 import math
35 35 import logging
36 36 import re
37 37 import time
38 38 import string
39 39 import hashlib
40 40 from collections import OrderedDict
41 41
42 42 import pygments
43 43 import itertools
44 44 import fnmatch
45 45 import bleach
46 46
47 47 from pyramid import compat
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55
56 56 from webhelpers2.html import literal, HTML, escape
57 57 from webhelpers2.html._autolink import _auto_link_urls
58 58 from webhelpers2.html.tools import (
59 59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 60
61 61 from webhelpers2.text import (
62 62 chop_at, collapse, convert_accented_entities,
63 63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 65 from webhelpers2.date import time_ago_in_words
66 66
67 67 from webhelpers2.html.tags import (
68 68 _input, NotGiven, _make_safe_id_component as safeid,
69 69 form as insecure_form,
70 70 auto_discovery_link, checkbox, end_form, file,
71 71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 72 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 73 ul, radio, Options)
74 74
75 75 from webhelpers2.number import format_byte_size
76 76
77 77 from rhodecode.lib.action_parser import action_parser
78 78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 79 from rhodecode.lib.ext_json import json
80 80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 81 from rhodecode.lib.utils2 import (
82 82 str2bool, safe_unicode, safe_str,
83 83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 90 from rhodecode.model.changeset_status import ChangesetStatusModel
91 91 from rhodecode.model.db import Permission, User, Repository
92 92 from rhodecode.model.repo_group import RepoGroupModel
93 93 from rhodecode.model.settings import IssueTrackerSettingsModel
94 94
95 95
96 96 log = logging.getLogger(__name__)
97 97
98 98
99 99 DEFAULT_USER = User.DEFAULT_USER
100 100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101 101
102 102
103 103 def asset(path, ver=None, **kwargs):
104 104 """
105 105 Helper to generate a static asset file path for rhodecode assets
106 106
107 107 eg. h.asset('images/image.png', ver='3923')
108 108
109 109 :param path: path of asset
110 110 :param ver: optional version query param to append as ?ver=
111 111 """
112 112 request = get_current_request()
113 113 query = {}
114 114 query.update(kwargs)
115 115 if ver:
116 116 query = {'ver': ver}
117 117 return request.static_path(
118 118 'rhodecode:public/{}'.format(path), _query=query)
119 119
120 120
121 121 default_html_escape_table = {
122 122 ord('&'): u'&amp;',
123 123 ord('<'): u'&lt;',
124 124 ord('>'): u'&gt;',
125 125 ord('"'): u'&quot;',
126 126 ord("'"): u'&#39;',
127 127 }
128 128
129 129
130 130 def html_escape(text, html_escape_table=default_html_escape_table):
131 131 """Produce entities within text."""
132 132 return text.translate(html_escape_table)
133 133
134 134
135 135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 136 """
137 137 Truncate string ``s`` at the first occurrence of ``sub``.
138 138
139 139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 140 """
141 141 suffix_if_chopped = suffix_if_chopped or ''
142 142 pos = s.find(sub)
143 143 if pos == -1:
144 144 return s
145 145
146 146 if inclusive:
147 147 pos += len(sub)
148 148
149 149 chopped = s[:pos]
150 150 left = s[pos:].strip()
151 151
152 152 if left and suffix_if_chopped:
153 153 chopped += suffix_if_chopped
154 154
155 155 return chopped
156 156
157 157
158 158 def shorter(text, size=20, prefix=False):
159 159 postfix = '...'
160 160 if len(text) > size:
161 161 if prefix:
162 162 # shorten in front
163 163 return postfix + text[-(size - len(postfix)):]
164 164 else:
165 165 return text[:size - len(postfix)] + postfix
166 166 return text
167 167
168 168
169 169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 170 """
171 171 Reset button
172 172 """
173 173 return _input(type, name, value, id, attrs)
174 174
175 175
176 176 def select(name, selected_values, options, id=NotGiven, **attrs):
177 177
178 178 if isinstance(options, (list, tuple)):
179 179 options_iter = options
180 180 # Handle old value,label lists ... where value also can be value,label lists
181 181 options = Options()
182 182 for opt in options_iter:
183 183 if isinstance(opt, tuple) and len(opt) == 2:
184 184 value, label = opt
185 185 elif isinstance(opt, basestring):
186 186 value = label = opt
187 187 else:
188 188 raise ValueError('invalid select option type %r' % type(opt))
189 189
190 190 if isinstance(value, (list, tuple)):
191 191 option_group = options.add_optgroup(label)
192 192 for opt2 in value:
193 193 if isinstance(opt2, tuple) and len(opt2) == 2:
194 194 group_value, group_label = opt2
195 195 elif isinstance(opt2, basestring):
196 196 group_value = group_label = opt2
197 197 else:
198 198 raise ValueError('invalid select option type %r' % type(opt2))
199 199
200 200 option_group.add_option(group_label, group_value)
201 201 else:
202 202 options.add_option(label, value)
203 203
204 204 return raw_select(name, selected_values, options, id=id, **attrs)
205 205
206 206
207 207 def branding(name, length=40):
208 208 return truncate(name, length, indicator="")
209 209
210 210
211 211 def FID(raw_id, path):
212 212 """
213 213 Creates a unique ID for filenode based on it's hash of path and commit
214 214 it's safe to use in urls
215 215
216 216 :param raw_id:
217 217 :param path:
218 218 """
219 219
220 220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221 221
222 222
223 223 class _GetError(object):
224 224 """Get error from form_errors, and represent it as span wrapped error
225 225 message
226 226
227 227 :param field_name: field to fetch errors for
228 228 :param form_errors: form errors dict
229 229 """
230 230
231 231 def __call__(self, field_name, form_errors):
232 232 tmpl = """<span class="error_msg">%s</span>"""
233 233 if form_errors and field_name in form_errors:
234 234 return literal(tmpl % form_errors.get(field_name))
235 235
236 236
237 237 get_error = _GetError()
238 238
239 239
240 240 class _ToolTip(object):
241 241
242 242 def __call__(self, tooltip_title, trim_at=50):
243 243 """
244 244 Special function just to wrap our text into nice formatted
245 245 autowrapped text
246 246
247 247 :param tooltip_title:
248 248 """
249 249 tooltip_title = escape(tooltip_title)
250 250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 251 return tooltip_title
252 252
253 253
254 254 tooltip = _ToolTip()
255 255
256 256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
257 257
258 258
259 259 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
260 260 if isinstance(file_path, str):
261 261 file_path = safe_unicode(file_path)
262 262
263 263 route_qry = {'at': at_ref} if at_ref else None
264 264
265 265 # first segment is a `..` link to repo files
266 266 root_name = literal(u'<i class="icon-home"></i>')
267 267 url_segments = [
268 268 link_to(
269 269 root_name,
270 270 route_path(
271 271 'repo_files',
272 272 repo_name=repo_name,
273 273 commit_id=commit_id,
274 274 f_path='',
275 275 _query=route_qry),
276 276 )]
277 277
278 278 path_segments = file_path.split('/')
279 279 last_cnt = len(path_segments) - 1
280 280 for cnt, segment in enumerate(path_segments):
281 281 if not segment:
282 282 continue
283 283 segment_html = escape(segment)
284 284
285 285 last_item = cnt == last_cnt
286 286
287 287 if last_item and linkify_last_item is False:
288 288 # plain version
289 289 url_segments.append(segment_html)
290 290 else:
291 291 url_segments.append(
292 292 link_to(
293 293 segment_html,
294 294 route_path(
295 295 'repo_files',
296 296 repo_name=repo_name,
297 297 commit_id=commit_id,
298 298 f_path='/'.join(path_segments[:cnt + 1]),
299 299 _query=route_qry),
300 300 ))
301 301
302 302 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
303 303 if limit_items and len(limited_url_segments) < len(url_segments):
304 304 url_segments = limited_url_segments
305 305
306 306 full_path = file_path
307 307 icon = files_icon.format(escape(full_path))
308 308 if file_path == '':
309 309 return root_name
310 310 else:
311 311 return literal(' / '.join(url_segments) + icon)
312 312
313 313
314 314 def files_url_data(request):
315 315 matchdict = request.matchdict
316 316
317 317 if 'f_path' not in matchdict:
318 318 matchdict['f_path'] = ''
319 319
320 320 if 'commit_id' not in matchdict:
321 321 matchdict['commit_id'] = 'tip'
322 322
323 323 return json.dumps(matchdict)
324 324
325 325
326 326 def code_highlight(code, lexer, formatter, use_hl_filter=False):
327 327 """
328 328 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
329 329
330 330 If ``outfile`` is given and a valid file object (an object
331 331 with a ``write`` method), the result will be written to it, otherwise
332 332 it is returned as a string.
333 333 """
334 334 if use_hl_filter:
335 335 # add HL filter
336 336 from rhodecode.lib.index import search_utils
337 337 lexer.add_filter(search_utils.ElasticSearchHLFilter())
338 338 return pygments.format(pygments.lex(code, lexer), formatter)
339 339
340 340
341 341 class CodeHtmlFormatter(HtmlFormatter):
342 342 """
343 343 My code Html Formatter for source codes
344 344 """
345 345
346 346 def wrap(self, source, outfile):
347 347 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
348 348
349 349 def _wrap_code(self, source):
350 350 for cnt, it in enumerate(source):
351 351 i, t = it
352 352 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
353 353 yield i, t
354 354
355 355 def _wrap_tablelinenos(self, inner):
356 356 dummyoutfile = StringIO.StringIO()
357 357 lncount = 0
358 358 for t, line in inner:
359 359 if t:
360 360 lncount += 1
361 361 dummyoutfile.write(line)
362 362
363 363 fl = self.linenostart
364 364 mw = len(str(lncount + fl - 1))
365 365 sp = self.linenospecial
366 366 st = self.linenostep
367 367 la = self.lineanchors
368 368 aln = self.anchorlinenos
369 369 nocls = self.noclasses
370 370 if sp:
371 371 lines = []
372 372
373 373 for i in range(fl, fl + lncount):
374 374 if i % st == 0:
375 375 if i % sp == 0:
376 376 if aln:
377 377 lines.append('<a href="#%s%d" class="special">%*d</a>' %
378 378 (la, i, mw, i))
379 379 else:
380 380 lines.append('<span class="special">%*d</span>' % (mw, i))
381 381 else:
382 382 if aln:
383 383 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
384 384 else:
385 385 lines.append('%*d' % (mw, i))
386 386 else:
387 387 lines.append('')
388 388 ls = '\n'.join(lines)
389 389 else:
390 390 lines = []
391 391 for i in range(fl, fl + lncount):
392 392 if i % st == 0:
393 393 if aln:
394 394 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
395 395 else:
396 396 lines.append('%*d' % (mw, i))
397 397 else:
398 398 lines.append('')
399 399 ls = '\n'.join(lines)
400 400
401 401 # in case you wonder about the seemingly redundant <div> here: since the
402 402 # content in the other cell also is wrapped in a div, some browsers in
403 403 # some configurations seem to mess up the formatting...
404 404 if nocls:
405 405 yield 0, ('<table class="%stable">' % self.cssclass +
406 406 '<tr><td><div class="linenodiv" '
407 407 'style="background-color: #f0f0f0; padding-right: 10px">'
408 408 '<pre style="line-height: 125%">' +
409 409 ls + '</pre></div></td><td id="hlcode" class="code">')
410 410 else:
411 411 yield 0, ('<table class="%stable">' % self.cssclass +
412 412 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
413 413 ls + '</pre></div></td><td id="hlcode" class="code">')
414 414 yield 0, dummyoutfile.getvalue()
415 415 yield 0, '</td></tr></table>'
416 416
417 417
418 418 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
419 419 def __init__(self, **kw):
420 420 # only show these line numbers if set
421 421 self.only_lines = kw.pop('only_line_numbers', [])
422 422 self.query_terms = kw.pop('query_terms', [])
423 423 self.max_lines = kw.pop('max_lines', 5)
424 424 self.line_context = kw.pop('line_context', 3)
425 425 self.url = kw.pop('url', None)
426 426
427 427 super(CodeHtmlFormatter, self).__init__(**kw)
428 428
429 429 def _wrap_code(self, source):
430 430 for cnt, it in enumerate(source):
431 431 i, t = it
432 432 t = '<pre>%s</pre>' % t
433 433 yield i, t
434 434
435 435 def _wrap_tablelinenos(self, inner):
436 436 yield 0, '<table class="code-highlight %stable">' % self.cssclass
437 437
438 438 last_shown_line_number = 0
439 439 current_line_number = 1
440 440
441 441 for t, line in inner:
442 442 if not t:
443 443 yield t, line
444 444 continue
445 445
446 446 if current_line_number in self.only_lines:
447 447 if last_shown_line_number + 1 != current_line_number:
448 448 yield 0, '<tr>'
449 449 yield 0, '<td class="line">...</td>'
450 450 yield 0, '<td id="hlcode" class="code"></td>'
451 451 yield 0, '</tr>'
452 452
453 453 yield 0, '<tr>'
454 454 if self.url:
455 455 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
456 456 self.url, current_line_number, current_line_number)
457 457 else:
458 458 yield 0, '<td class="line"><a href="">%i</a></td>' % (
459 459 current_line_number)
460 460 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
461 461 yield 0, '</tr>'
462 462
463 463 last_shown_line_number = current_line_number
464 464
465 465 current_line_number += 1
466 466
467 467 yield 0, '</table>'
468 468
469 469
470 470 def hsv_to_rgb(h, s, v):
471 471 """ Convert hsv color values to rgb """
472 472
473 473 if s == 0.0:
474 474 return v, v, v
475 475 i = int(h * 6.0) # XXX assume int() truncates!
476 476 f = (h * 6.0) - i
477 477 p = v * (1.0 - s)
478 478 q = v * (1.0 - s * f)
479 479 t = v * (1.0 - s * (1.0 - f))
480 480 i = i % 6
481 481 if i == 0:
482 482 return v, t, p
483 483 if i == 1:
484 484 return q, v, p
485 485 if i == 2:
486 486 return p, v, t
487 487 if i == 3:
488 488 return p, q, v
489 489 if i == 4:
490 490 return t, p, v
491 491 if i == 5:
492 492 return v, p, q
493 493
494 494
495 495 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
496 496 """
497 497 Generator for getting n of evenly distributed colors using
498 498 hsv color and golden ratio. It always return same order of colors
499 499
500 500 :param n: number of colors to generate
501 501 :param saturation: saturation of returned colors
502 502 :param lightness: lightness of returned colors
503 503 :returns: RGB tuple
504 504 """
505 505
506 506 golden_ratio = 0.618033988749895
507 507 h = 0.22717784590367374
508 508
509 509 for _ in xrange(n):
510 510 h += golden_ratio
511 511 h %= 1
512 512 HSV_tuple = [h, saturation, lightness]
513 513 RGB_tuple = hsv_to_rgb(*HSV_tuple)
514 514 yield map(lambda x: str(int(x * 256)), RGB_tuple)
515 515
516 516
517 517 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
518 518 """
519 519 Returns a function which when called with an argument returns a unique
520 520 color for that argument, eg.
521 521
522 522 :param n: number of colors to generate
523 523 :param saturation: saturation of returned colors
524 524 :param lightness: lightness of returned colors
525 525 :returns: css RGB string
526 526
527 527 >>> color_hash = color_hasher()
528 528 >>> color_hash('hello')
529 529 'rgb(34, 12, 59)'
530 530 >>> color_hash('hello')
531 531 'rgb(34, 12, 59)'
532 532 >>> color_hash('other')
533 533 'rgb(90, 224, 159)'
534 534 """
535 535
536 536 color_dict = {}
537 537 cgenerator = unique_color_generator(
538 538 saturation=saturation, lightness=lightness)
539 539
540 540 def get_color_string(thing):
541 541 if thing in color_dict:
542 542 col = color_dict[thing]
543 543 else:
544 544 col = color_dict[thing] = cgenerator.next()
545 545 return "rgb(%s)" % (', '.join(col))
546 546
547 547 return get_color_string
548 548
549 549
550 550 def get_lexer_safe(mimetype=None, filepath=None):
551 551 """
552 552 Tries to return a relevant pygments lexer using mimetype/filepath name,
553 553 defaulting to plain text if none could be found
554 554 """
555 555 lexer = None
556 556 try:
557 557 if mimetype:
558 558 lexer = get_lexer_for_mimetype(mimetype)
559 559 if not lexer:
560 560 lexer = get_lexer_for_filename(filepath)
561 561 except pygments.util.ClassNotFound:
562 562 pass
563 563
564 564 if not lexer:
565 565 lexer = get_lexer_by_name('text')
566 566
567 567 return lexer
568 568
569 569
570 570 def get_lexer_for_filenode(filenode):
571 571 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
572 572 return lexer
573 573
574 574
575 575 def pygmentize(filenode, **kwargs):
576 576 """
577 577 pygmentize function using pygments
578 578
579 579 :param filenode:
580 580 """
581 581 lexer = get_lexer_for_filenode(filenode)
582 582 return literal(code_highlight(filenode.content, lexer,
583 583 CodeHtmlFormatter(**kwargs)))
584 584
585 585
586 586 def is_following_repo(repo_name, user_id):
587 587 from rhodecode.model.scm import ScmModel
588 588 return ScmModel().is_following_repo(repo_name, user_id)
589 589
590 590
591 591 class _Message(object):
592 592 """A message returned by ``Flash.pop_messages()``.
593 593
594 594 Converting the message to a string returns the message text. Instances
595 595 also have the following attributes:
596 596
597 597 * ``message``: the message text.
598 598 * ``category``: the category specified when the message was created.
599 599 """
600 600
601 601 def __init__(self, category, message):
602 602 self.category = category
603 603 self.message = message
604 604
605 605 def __str__(self):
606 606 return self.message
607 607
608 608 __unicode__ = __str__
609 609
610 610 def __html__(self):
611 611 return escape(safe_unicode(self.message))
612 612
613 613
614 614 class Flash(object):
615 615 # List of allowed categories. If None, allow any category.
616 616 categories = ["warning", "notice", "error", "success"]
617 617
618 618 # Default category if none is specified.
619 619 default_category = "notice"
620 620
621 621 def __init__(self, session_key="flash", categories=None,
622 622 default_category=None):
623 623 """
624 624 Instantiate a ``Flash`` object.
625 625
626 626 ``session_key`` is the key to save the messages under in the user's
627 627 session.
628 628
629 629 ``categories`` is an optional list which overrides the default list
630 630 of categories.
631 631
632 632 ``default_category`` overrides the default category used for messages
633 633 when none is specified.
634 634 """
635 635 self.session_key = session_key
636 636 if categories is not None:
637 637 self.categories = categories
638 638 if default_category is not None:
639 639 self.default_category = default_category
640 640 if self.categories and self.default_category not in self.categories:
641 641 raise ValueError(
642 642 "unrecognized default category %r" % (self.default_category,))
643 643
644 644 def pop_messages(self, session=None, request=None):
645 645 """
646 646 Return all accumulated messages and delete them from the session.
647 647
648 648 The return value is a list of ``Message`` objects.
649 649 """
650 650 messages = []
651 651
652 652 if not session:
653 653 if not request:
654 654 request = get_current_request()
655 655 session = request.session
656 656
657 657 # Pop the 'old' pylons flash messages. They are tuples of the form
658 658 # (category, message)
659 659 for cat, msg in session.pop(self.session_key, []):
660 660 messages.append(_Message(cat, msg))
661 661
662 662 # Pop the 'new' pyramid flash messages for each category as list
663 663 # of strings.
664 664 for cat in self.categories:
665 665 for msg in session.pop_flash(queue=cat):
666 666 messages.append(_Message(cat, msg))
667 667 # Map messages from the default queue to the 'notice' category.
668 668 for msg in session.pop_flash():
669 669 messages.append(_Message('notice', msg))
670 670
671 671 session.save()
672 672 return messages
673 673
674 674 def json_alerts(self, session=None, request=None):
675 675 payloads = []
676 676 messages = flash.pop_messages(session=session, request=request)
677 677 if messages:
678 678 for message in messages:
679 679 subdata = {}
680 680 if hasattr(message.message, 'rsplit'):
681 681 flash_data = message.message.rsplit('|DELIM|', 1)
682 682 org_message = flash_data[0]
683 683 if len(flash_data) > 1:
684 684 subdata = json.loads(flash_data[1])
685 685 else:
686 686 org_message = message.message
687 687 payloads.append({
688 688 'message': {
689 689 'message': u'{}'.format(org_message),
690 690 'level': message.category,
691 691 'force': True,
692 692 'subdata': subdata
693 693 }
694 694 })
695 695 return json.dumps(payloads)
696 696
697 697 def __call__(self, message, category=None, ignore_duplicate=True,
698 698 session=None, request=None):
699 699
700 700 if not session:
701 701 if not request:
702 702 request = get_current_request()
703 703 session = request.session
704 704
705 705 session.flash(
706 706 message, queue=category, allow_duplicate=not ignore_duplicate)
707 707
708 708
709 709 flash = Flash()
710 710
711 711 #==============================================================================
712 712 # SCM FILTERS available via h.
713 713 #==============================================================================
714 714 from rhodecode.lib.vcs.utils import author_name, author_email
715 715 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
716 716 from rhodecode.model.db import User, ChangesetStatus
717 717
718 718 capitalize = lambda x: x.capitalize()
719 719 email = author_email
720 720 short_id = lambda x: x[:12]
721 721 hide_credentials = lambda x: ''.join(credentials_filter(x))
722 722
723 723
724 724 import pytz
725 725 import tzlocal
726 726 local_timezone = tzlocal.get_localzone()
727 727
728 728
729 def age_component(datetime_iso, value=None, time_is_local=False):
729 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
730 730 title = value or format_date(datetime_iso)
731 731 tzinfo = '+00:00'
732 732
733 733 # detect if we have a timezone info, otherwise, add it
734 734 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
735 735 force_timezone = os.environ.get('RC_TIMEZONE', '')
736 736 if force_timezone:
737 737 force_timezone = pytz.timezone(force_timezone)
738 738 timezone = force_timezone or local_timezone
739 739 offset = timezone.localize(datetime_iso).strftime('%z')
740 740 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
741 741
742 742 return literal(
743 '<time class="timeago tooltip" '
744 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
745 datetime_iso, title, tzinfo))
743 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
744 cls='tooltip' if tooltip else '',
745 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
746 title=title, dt=datetime_iso, tzinfo=tzinfo
747 ))
746 748
747 749
748 750 def _shorten_commit_id(commit_id, commit_len=None):
749 751 if commit_len is None:
750 752 request = get_current_request()
751 753 commit_len = request.call_context.visual.show_sha_length
752 754 return commit_id[:commit_len]
753 755
754 756
755 757 def show_id(commit, show_idx=None, commit_len=None):
756 758 """
757 759 Configurable function that shows ID
758 760 by default it's r123:fffeeefffeee
759 761
760 762 :param commit: commit instance
761 763 """
762 764 if show_idx is None:
763 765 request = get_current_request()
764 766 show_idx = request.call_context.visual.show_revision_number
765 767
766 768 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
767 769 if show_idx:
768 770 return 'r%s:%s' % (commit.idx, raw_id)
769 771 else:
770 772 return '%s' % (raw_id, )
771 773
772 774
773 775 def format_date(date):
774 776 """
775 777 use a standardized formatting for dates used in RhodeCode
776 778
777 779 :param date: date/datetime object
778 780 :return: formatted date
779 781 """
780 782
781 783 if date:
782 784 _fmt = "%a, %d %b %Y %H:%M:%S"
783 785 return safe_unicode(date.strftime(_fmt))
784 786
785 787 return u""
786 788
787 789
788 790 class _RepoChecker(object):
789 791
790 792 def __init__(self, backend_alias):
791 793 self._backend_alias = backend_alias
792 794
793 795 def __call__(self, repository):
794 796 if hasattr(repository, 'alias'):
795 797 _type = repository.alias
796 798 elif hasattr(repository, 'repo_type'):
797 799 _type = repository.repo_type
798 800 else:
799 801 _type = repository
800 802 return _type == self._backend_alias
801 803
802 804
803 805 is_git = _RepoChecker('git')
804 806 is_hg = _RepoChecker('hg')
805 807 is_svn = _RepoChecker('svn')
806 808
807 809
808 810 def get_repo_type_by_name(repo_name):
809 811 repo = Repository.get_by_repo_name(repo_name)
810 812 if repo:
811 813 return repo.repo_type
812 814
813 815
814 816 def is_svn_without_proxy(repository):
815 817 if is_svn(repository):
816 818 from rhodecode.model.settings import VcsSettingsModel
817 819 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
818 820 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
819 821 return False
820 822
821 823
822 824 def discover_user(author):
823 825 """
824 826 Tries to discover RhodeCode User based on the author string. Author string
825 827 is typically `FirstName LastName <email@address.com>`
826 828 """
827 829
828 830 # if author is already an instance use it for extraction
829 831 if isinstance(author, User):
830 832 return author
831 833
832 834 # Valid email in the attribute passed, see if they're in the system
833 835 _email = author_email(author)
834 836 if _email != '':
835 837 user = User.get_by_email(_email, case_insensitive=True, cache=True)
836 838 if user is not None:
837 839 return user
838 840
839 841 # Maybe it's a username, we try to extract it and fetch by username ?
840 842 _author = author_name(author)
841 843 user = User.get_by_username(_author, case_insensitive=True, cache=True)
842 844 if user is not None:
843 845 return user
844 846
845 847 return None
846 848
847 849
848 850 def email_or_none(author):
849 851 # extract email from the commit string
850 852 _email = author_email(author)
851 853
852 854 # If we have an email, use it, otherwise
853 855 # see if it contains a username we can get an email from
854 856 if _email != '':
855 857 return _email
856 858 else:
857 859 user = User.get_by_username(
858 860 author_name(author), case_insensitive=True, cache=True)
859 861
860 862 if user is not None:
861 863 return user.email
862 864
863 865 # No valid email, not a valid user in the system, none!
864 866 return None
865 867
866 868
867 869 def link_to_user(author, length=0, **kwargs):
868 870 user = discover_user(author)
869 871 # user can be None, but if we have it already it means we can re-use it
870 872 # in the person() function, so we save 1 intensive-query
871 873 if user:
872 874 author = user
873 875
874 876 display_person = person(author, 'username_or_name_or_email')
875 877 if length:
876 878 display_person = shorter(display_person, length)
877 879
878 880 if user:
879 881 return link_to(
880 882 escape(display_person),
881 883 route_path('user_profile', username=user.username),
882 884 **kwargs)
883 885 else:
884 886 return escape(display_person)
885 887
886 888
887 889 def link_to_group(users_group_name, **kwargs):
888 890 return link_to(
889 891 escape(users_group_name),
890 892 route_path('user_group_profile', user_group_name=users_group_name),
891 893 **kwargs)
892 894
893 895
894 896 def person(author, show_attr="username_and_name"):
895 897 user = discover_user(author)
896 898 if user:
897 899 return getattr(user, show_attr)
898 900 else:
899 901 _author = author_name(author)
900 902 _email = email(author)
901 903 return _author or _email
902 904
903 905
904 906 def author_string(email):
905 907 if email:
906 908 user = User.get_by_email(email, case_insensitive=True, cache=True)
907 909 if user:
908 910 if user.first_name or user.last_name:
909 911 return '%s %s &lt;%s&gt;' % (
910 912 user.first_name, user.last_name, email)
911 913 else:
912 914 return email
913 915 else:
914 916 return email
915 917 else:
916 918 return None
917 919
918 920
919 921 def person_by_id(id_, show_attr="username_and_name"):
920 922 # attr to return from fetched user
921 923 person_getter = lambda usr: getattr(usr, show_attr)
922 924
923 925 #maybe it's an ID ?
924 926 if str(id_).isdigit() or isinstance(id_, int):
925 927 id_ = int(id_)
926 928 user = User.get(id_)
927 929 if user is not None:
928 930 return person_getter(user)
929 931 return id_
930 932
931 933
932 934 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
933 935 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
934 936 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
935 937
936 938
937 939 tags_paterns = OrderedDict((
938 940 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
939 941 '<div class="metatag" tag="lang">\\2</div>')),
940 942
941 943 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
942 944 '<div class="metatag" tag="see">see: \\1 </div>')),
943 945
944 946 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
945 947 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
946 948
947 949 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
948 950 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
949 951
950 952 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
951 953 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
952 954
953 955 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
954 956 '<div class="metatag" tag="state \\1">\\1</div>')),
955 957
956 958 # label in grey
957 959 ('label', (re.compile(r'\[([a-z]+)\]'),
958 960 '<div class="metatag" tag="label">\\1</div>')),
959 961
960 962 # generic catch all in grey
961 963 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
962 964 '<div class="metatag" tag="generic">\\1</div>')),
963 965 ))
964 966
965 967
966 968 def extract_metatags(value):
967 969 """
968 970 Extract supported meta-tags from given text value
969 971 """
970 972 tags = []
971 973 if not value:
972 974 return tags, ''
973 975
974 976 for key, val in tags_paterns.items():
975 977 pat, replace_html = val
976 978 tags.extend([(key, x.group()) for x in pat.finditer(value)])
977 979 value = pat.sub('', value)
978 980
979 981 return tags, value
980 982
981 983
982 984 def style_metatag(tag_type, value):
983 985 """
984 986 converts tags from value into html equivalent
985 987 """
986 988 if not value:
987 989 return ''
988 990
989 991 html_value = value
990 992 tag_data = tags_paterns.get(tag_type)
991 993 if tag_data:
992 994 pat, replace_html = tag_data
993 995 # convert to plain `unicode` instead of a markup tag to be used in
994 996 # regex expressions. safe_unicode doesn't work here
995 997 html_value = pat.sub(replace_html, unicode(value))
996 998
997 999 return html_value
998 1000
999 1001
1000 1002 def bool2icon(value, show_at_false=True):
1001 1003 """
1002 1004 Returns boolean value of a given value, represented as html element with
1003 1005 classes that will represent icons
1004 1006
1005 1007 :param value: given value to convert to html node
1006 1008 """
1007 1009
1008 1010 if value: # does bool conversion
1009 1011 return HTML.tag('i', class_="icon-true", title='True')
1010 1012 else: # not true as bool
1011 1013 if show_at_false:
1012 1014 return HTML.tag('i', class_="icon-false", title='False')
1013 1015 return HTML.tag('i')
1014 1016
1015 1017 #==============================================================================
1016 1018 # PERMS
1017 1019 #==============================================================================
1018 1020 from rhodecode.lib.auth import (
1019 1021 HasPermissionAny, HasPermissionAll,
1020 1022 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1021 1023 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1022 1024 csrf_token_key, AuthUser)
1023 1025
1024 1026
1025 1027 #==============================================================================
1026 1028 # GRAVATAR URL
1027 1029 #==============================================================================
1028 1030 class InitialsGravatar(object):
1029 1031 def __init__(self, email_address, first_name, last_name, size=30,
1030 1032 background=None, text_color='#fff'):
1031 1033 self.size = size
1032 1034 self.first_name = first_name
1033 1035 self.last_name = last_name
1034 1036 self.email_address = email_address
1035 1037 self.background = background or self.str2color(email_address)
1036 1038 self.text_color = text_color
1037 1039
1038 1040 def get_color_bank(self):
1039 1041 """
1040 1042 returns a predefined list of colors that gravatars can use.
1041 1043 Those are randomized distinct colors that guarantee readability and
1042 1044 uniqueness.
1043 1045
1044 1046 generated with: http://phrogz.net/css/distinct-colors.html
1045 1047 """
1046 1048 return [
1047 1049 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1048 1050 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1049 1051 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1050 1052 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1051 1053 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1052 1054 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1053 1055 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1054 1056 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1055 1057 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1056 1058 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1057 1059 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1058 1060 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1059 1061 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1060 1062 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1061 1063 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1062 1064 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1063 1065 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1064 1066 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1065 1067 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1066 1068 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1067 1069 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1068 1070 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1069 1071 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1070 1072 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1071 1073 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1072 1074 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1073 1075 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1074 1076 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1075 1077 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1076 1078 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1077 1079 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1078 1080 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1079 1081 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1080 1082 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1081 1083 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1082 1084 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1083 1085 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1084 1086 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1085 1087 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1086 1088 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1087 1089 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1088 1090 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1089 1091 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1090 1092 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1091 1093 '#4f8c46', '#368dd9', '#5c0073'
1092 1094 ]
1093 1095
1094 1096 def rgb_to_hex_color(self, rgb_tuple):
1095 1097 """
1096 1098 Converts an rgb_tuple passed to an hex color.
1097 1099
1098 1100 :param rgb_tuple: tuple with 3 ints represents rgb color space
1099 1101 """
1100 1102 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1101 1103
1102 1104 def email_to_int_list(self, email_str):
1103 1105 """
1104 1106 Get every byte of the hex digest value of email and turn it to integer.
1105 1107 It's going to be always between 0-255
1106 1108 """
1107 1109 digest = md5_safe(email_str.lower())
1108 1110 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1109 1111
1110 1112 def pick_color_bank_index(self, email_str, color_bank):
1111 1113 return self.email_to_int_list(email_str)[0] % len(color_bank)
1112 1114
1113 1115 def str2color(self, email_str):
1114 1116 """
1115 1117 Tries to map in a stable algorithm an email to color
1116 1118
1117 1119 :param email_str:
1118 1120 """
1119 1121 color_bank = self.get_color_bank()
1120 1122 # pick position (module it's length so we always find it in the
1121 1123 # bank even if it's smaller than 256 values
1122 1124 pos = self.pick_color_bank_index(email_str, color_bank)
1123 1125 return color_bank[pos]
1124 1126
1125 1127 def normalize_email(self, email_address):
1126 1128 import unicodedata
1127 1129 # default host used to fill in the fake/missing email
1128 1130 default_host = u'localhost'
1129 1131
1130 1132 if not email_address:
1131 1133 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1132 1134
1133 1135 email_address = safe_unicode(email_address)
1134 1136
1135 1137 if u'@' not in email_address:
1136 1138 email_address = u'%s@%s' % (email_address, default_host)
1137 1139
1138 1140 if email_address.endswith(u'@'):
1139 1141 email_address = u'%s%s' % (email_address, default_host)
1140 1142
1141 1143 email_address = unicodedata.normalize('NFKD', email_address)\
1142 1144 .encode('ascii', 'ignore')
1143 1145 return email_address
1144 1146
1145 1147 def get_initials(self):
1146 1148 """
1147 1149 Returns 2 letter initials calculated based on the input.
1148 1150 The algorithm picks first given email address, and takes first letter
1149 1151 of part before @, and then the first letter of server name. In case
1150 1152 the part before @ is in a format of `somestring.somestring2` it replaces
1151 1153 the server letter with first letter of somestring2
1152 1154
1153 1155 In case function was initialized with both first and lastname, this
1154 1156 overrides the extraction from email by first letter of the first and
1155 1157 last name. We add special logic to that functionality, In case Full name
1156 1158 is compound, like Guido Von Rossum, we use last part of the last name
1157 1159 (Von Rossum) picking `R`.
1158 1160
1159 1161 Function also normalizes the non-ascii characters to they ascii
1160 1162 representation, eg Δ„ => A
1161 1163 """
1162 1164 import unicodedata
1163 1165 # replace non-ascii to ascii
1164 1166 first_name = unicodedata.normalize(
1165 1167 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1166 1168 last_name = unicodedata.normalize(
1167 1169 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1168 1170
1169 1171 # do NFKD encoding, and also make sure email has proper format
1170 1172 email_address = self.normalize_email(self.email_address)
1171 1173
1172 1174 # first push the email initials
1173 1175 prefix, server = email_address.split('@', 1)
1174 1176
1175 1177 # check if prefix is maybe a 'first_name.last_name' syntax
1176 1178 _dot_split = prefix.rsplit('.', 1)
1177 1179 if len(_dot_split) == 2 and _dot_split[1]:
1178 1180 initials = [_dot_split[0][0], _dot_split[1][0]]
1179 1181 else:
1180 1182 initials = [prefix[0], server[0]]
1181 1183
1182 1184 # then try to replace either first_name or last_name
1183 1185 fn_letter = (first_name or " ")[0].strip()
1184 1186 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1185 1187
1186 1188 if fn_letter:
1187 1189 initials[0] = fn_letter
1188 1190
1189 1191 if ln_letter:
1190 1192 initials[1] = ln_letter
1191 1193
1192 1194 return ''.join(initials).upper()
1193 1195
1194 1196 def get_img_data_by_type(self, font_family, img_type):
1195 1197 default_user = """
1196 1198 <svg xmlns="http://www.w3.org/2000/svg"
1197 1199 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1198 1200 viewBox="-15 -10 439.165 429.164"
1199 1201
1200 1202 xml:space="preserve"
1201 1203 style="background:{background};" >
1202 1204
1203 1205 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1204 1206 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1205 1207 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1206 1208 168.596,153.916,216.671,
1207 1209 204.583,216.671z" fill="{text_color}"/>
1208 1210 <path d="M407.164,374.717L360.88,
1209 1211 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1210 1212 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1211 1213 15.366-44.203,23.488-69.076,23.488c-24.877,
1212 1214 0-48.762-8.122-69.078-23.488
1213 1215 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1214 1216 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1215 1217 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1216 1218 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1217 1219 19.402-10.527 C409.699,390.129,
1218 1220 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1219 1221 </svg>""".format(
1220 1222 size=self.size,
1221 1223 background='#979797', # @grey4
1222 1224 text_color=self.text_color,
1223 1225 font_family=font_family)
1224 1226
1225 1227 return {
1226 1228 "default_user": default_user
1227 1229 }[img_type]
1228 1230
1229 1231 def get_img_data(self, svg_type=None):
1230 1232 """
1231 1233 generates the svg metadata for image
1232 1234 """
1233 1235 fonts = [
1234 1236 '-apple-system',
1235 1237 'BlinkMacSystemFont',
1236 1238 'Segoe UI',
1237 1239 'Roboto',
1238 1240 'Oxygen-Sans',
1239 1241 'Ubuntu',
1240 1242 'Cantarell',
1241 1243 'Helvetica Neue',
1242 1244 'sans-serif'
1243 1245 ]
1244 1246 font_family = ','.join(fonts)
1245 1247 if svg_type:
1246 1248 return self.get_img_data_by_type(font_family, svg_type)
1247 1249
1248 1250 initials = self.get_initials()
1249 1251 img_data = """
1250 1252 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1251 1253 width="{size}" height="{size}"
1252 1254 style="width: 100%; height: 100%; background-color: {background}"
1253 1255 viewBox="0 0 {size} {size}">
1254 1256 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1255 1257 pointer-events="auto" fill="{text_color}"
1256 1258 font-family="{font_family}"
1257 1259 style="font-weight: 400; font-size: {f_size}px;">{text}
1258 1260 </text>
1259 1261 </svg>""".format(
1260 1262 size=self.size,
1261 1263 f_size=self.size/2.05, # scale the text inside the box nicely
1262 1264 background=self.background,
1263 1265 text_color=self.text_color,
1264 1266 text=initials.upper(),
1265 1267 font_family=font_family)
1266 1268
1267 1269 return img_data
1268 1270
1269 1271 def generate_svg(self, svg_type=None):
1270 1272 img_data = self.get_img_data(svg_type)
1271 1273 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1272 1274
1273 1275
1274 1276 def initials_gravatar(email_address, first_name, last_name, size=30):
1275 1277 svg_type = None
1276 1278 if email_address == User.DEFAULT_USER_EMAIL:
1277 1279 svg_type = 'default_user'
1278 1280 klass = InitialsGravatar(email_address, first_name, last_name, size)
1279 1281 return klass.generate_svg(svg_type=svg_type)
1280 1282
1281 1283
1282 1284 def gravatar_url(email_address, size=30, request=None):
1283 1285 request = get_current_request()
1284 1286 _use_gravatar = request.call_context.visual.use_gravatar
1285 1287 _gravatar_url = request.call_context.visual.gravatar_url
1286 1288
1287 1289 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1288 1290
1289 1291 email_address = email_address or User.DEFAULT_USER_EMAIL
1290 1292 if isinstance(email_address, unicode):
1291 1293 # hashlib crashes on unicode items
1292 1294 email_address = safe_str(email_address)
1293 1295
1294 1296 # empty email or default user
1295 1297 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1296 1298 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1297 1299
1298 1300 if _use_gravatar:
1299 1301 # TODO: Disuse pyramid thread locals. Think about another solution to
1300 1302 # get the host and schema here.
1301 1303 request = get_current_request()
1302 1304 tmpl = safe_str(_gravatar_url)
1303 1305 tmpl = tmpl.replace('{email}', email_address)\
1304 1306 .replace('{md5email}', md5_safe(email_address.lower())) \
1305 1307 .replace('{netloc}', request.host)\
1306 1308 .replace('{scheme}', request.scheme)\
1307 1309 .replace('{size}', safe_str(size))
1308 1310 return tmpl
1309 1311 else:
1310 1312 return initials_gravatar(email_address, '', '', size=size)
1311 1313
1312 1314
1313 1315 def breadcrumb_repo_link(repo):
1314 1316 """
1315 1317 Makes a breadcrumbs path link to repo
1316 1318
1317 1319 ex::
1318 1320 group >> subgroup >> repo
1319 1321
1320 1322 :param repo: a Repository instance
1321 1323 """
1322 1324
1323 1325 path = [
1324 1326 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1325 1327 title='last change:{}'.format(format_date(group.last_commit_change)))
1326 1328 for group in repo.groups_with_parents
1327 1329 ] + [
1328 1330 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1329 1331 title='last change:{}'.format(format_date(repo.last_commit_change)))
1330 1332 ]
1331 1333
1332 1334 return literal(' &raquo; '.join(path))
1333 1335
1334 1336
1335 1337 def breadcrumb_repo_group_link(repo_group):
1336 1338 """
1337 1339 Makes a breadcrumbs path link to repo
1338 1340
1339 1341 ex::
1340 1342 group >> subgroup
1341 1343
1342 1344 :param repo_group: a Repository Group instance
1343 1345 """
1344 1346
1345 1347 path = [
1346 1348 link_to(group.name,
1347 1349 route_path('repo_group_home', repo_group_name=group.group_name),
1348 1350 title='last change:{}'.format(format_date(group.last_commit_change)))
1349 1351 for group in repo_group.parents
1350 1352 ] + [
1351 1353 link_to(repo_group.name,
1352 1354 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1353 1355 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1354 1356 ]
1355 1357
1356 1358 return literal(' &raquo; '.join(path))
1357 1359
1358 1360
1359 1361 def format_byte_size_binary(file_size):
1360 1362 """
1361 1363 Formats file/folder sizes to standard.
1362 1364 """
1363 1365 if file_size is None:
1364 1366 file_size = 0
1365 1367
1366 1368 formatted_size = format_byte_size(file_size, binary=True)
1367 1369 return formatted_size
1368 1370
1369 1371
1370 1372 def urlify_text(text_, safe=True, **href_attrs):
1371 1373 """
1372 1374 Extract urls from text and make html links out of them
1373 1375 """
1374 1376
1375 1377 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1376 1378 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1377 1379
1378 1380 def url_func(match_obj):
1379 1381 url_full = match_obj.groups()[0]
1380 1382 a_options = dict(href_attrs)
1381 1383 a_options['href'] = url_full
1382 1384 a_text = url_full
1383 1385 return HTML.tag("a", a_text, **a_options)
1384 1386
1385 1387 _new_text = url_pat.sub(url_func, text_)
1386 1388
1387 1389 if safe:
1388 1390 return literal(_new_text)
1389 1391 return _new_text
1390 1392
1391 1393
1392 1394 def urlify_commits(text_, repo_name):
1393 1395 """
1394 1396 Extract commit ids from text and make link from them
1395 1397
1396 1398 :param text_:
1397 1399 :param repo_name: repo name to build the URL with
1398 1400 """
1399 1401
1400 1402 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1401 1403
1402 1404 def url_func(match_obj):
1403 1405 commit_id = match_obj.groups()[1]
1404 1406 pref = match_obj.groups()[0]
1405 1407 suf = match_obj.groups()[2]
1406 1408
1407 1409 tmpl = (
1408 1410 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1409 1411 '%(commit_id)s</a>%(suf)s'
1410 1412 )
1411 1413 return tmpl % {
1412 1414 'pref': pref,
1413 1415 'cls': 'revision-link',
1414 1416 'url': route_url(
1415 1417 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1416 1418 'commit_id': commit_id,
1417 1419 'suf': suf,
1418 1420 'hovercard_alt': 'Commit: {}'.format(commit_id),
1419 1421 'hovercard_url': route_url(
1420 1422 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1421 1423 }
1422 1424
1423 1425 new_text = url_pat.sub(url_func, text_)
1424 1426
1425 1427 return new_text
1426 1428
1427 1429
1428 1430 def _process_url_func(match_obj, repo_name, uid, entry,
1429 1431 return_raw_data=False, link_format='html'):
1430 1432 pref = ''
1431 1433 if match_obj.group().startswith(' '):
1432 1434 pref = ' '
1433 1435
1434 1436 issue_id = ''.join(match_obj.groups())
1435 1437
1436 1438 if link_format == 'html':
1437 1439 tmpl = (
1438 1440 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1439 1441 '%(issue-prefix)s%(id-repr)s'
1440 1442 '</a>')
1441 1443 elif link_format == 'html+hovercard':
1442 1444 tmpl = (
1443 1445 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1444 1446 '%(issue-prefix)s%(id-repr)s'
1445 1447 '</a>')
1446 1448 elif link_format in ['rst', 'rst+hovercard']:
1447 1449 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1448 1450 elif link_format in ['markdown', 'markdown+hovercard']:
1449 1451 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1450 1452 else:
1451 1453 raise ValueError('Bad link_format:{}'.format(link_format))
1452 1454
1453 1455 (repo_name_cleaned,
1454 1456 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1455 1457
1456 1458 # variables replacement
1457 1459 named_vars = {
1458 1460 'id': issue_id,
1459 1461 'repo': repo_name,
1460 1462 'repo_name': repo_name_cleaned,
1461 1463 'group_name': parent_group_name,
1462 1464 # set dummy keys so we always have them
1463 1465 'hostname': '',
1464 1466 'netloc': '',
1465 1467 'scheme': ''
1466 1468 }
1467 1469
1468 1470 request = get_current_request()
1469 1471 if request:
1470 1472 # exposes, hostname, netloc, scheme
1471 1473 host_data = get_host_info(request)
1472 1474 named_vars.update(host_data)
1473 1475
1474 1476 # named regex variables
1475 1477 named_vars.update(match_obj.groupdict())
1476 1478 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1477 1479 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1478 1480 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1479 1481
1480 1482 def quote_cleaner(input_str):
1481 1483 """Remove quotes as it's HTML"""
1482 1484 return input_str.replace('"', '')
1483 1485
1484 1486 data = {
1485 1487 'pref': pref,
1486 1488 'cls': quote_cleaner('issue-tracker-link'),
1487 1489 'url': quote_cleaner(_url),
1488 1490 'id-repr': issue_id,
1489 1491 'issue-prefix': entry['pref'],
1490 1492 'serv': entry['url'],
1491 1493 'title': desc,
1492 1494 'hovercard_url': hovercard_url
1493 1495 }
1494 1496
1495 1497 if return_raw_data:
1496 1498 return {
1497 1499 'id': issue_id,
1498 1500 'url': _url
1499 1501 }
1500 1502 return tmpl % data
1501 1503
1502 1504
1503 1505 def get_active_pattern_entries(repo_name):
1504 1506 repo = None
1505 1507 if repo_name:
1506 1508 # Retrieving repo_name to avoid invalid repo_name to explode on
1507 1509 # IssueTrackerSettingsModel but still passing invalid name further down
1508 1510 repo = Repository.get_by_repo_name(repo_name, cache=True)
1509 1511
1510 1512 settings_model = IssueTrackerSettingsModel(repo=repo)
1511 1513 active_entries = settings_model.get_settings(cache=True)
1512 1514 return active_entries
1513 1515
1514 1516
1515 1517 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1516 1518
1517 1519 allowed_formats = ['html', 'rst', 'markdown',
1518 1520 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1519 1521 if link_format not in allowed_formats:
1520 1522 raise ValueError('Link format can be only one of:{} got {}'.format(
1521 1523 allowed_formats, link_format))
1522 1524
1523 1525 active_entries = active_entries or get_active_pattern_entries(repo_name)
1524 1526 issues_data = []
1525 1527 new_text = text_string
1526 1528
1527 1529 log.debug('Got %s entries to process', len(active_entries))
1528 1530 for uid, entry in active_entries.items():
1529 1531 log.debug('found issue tracker entry with uid %s', uid)
1530 1532
1531 1533 if not (entry['pat'] and entry['url']):
1532 1534 log.debug('skipping due to missing data')
1533 1535 continue
1534 1536
1535 1537 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1536 1538 uid, entry['pat'], entry['url'], entry['pref'])
1537 1539
1538 1540 try:
1539 1541 pattern = re.compile(r'%s' % entry['pat'])
1540 1542 except re.error:
1541 1543 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1542 1544 continue
1543 1545
1544 1546 data_func = partial(
1545 1547 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1546 1548 return_raw_data=True)
1547 1549
1548 1550 for match_obj in pattern.finditer(text_string):
1549 1551 issues_data.append(data_func(match_obj))
1550 1552
1551 1553 url_func = partial(
1552 1554 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1553 1555 link_format=link_format)
1554 1556
1555 1557 new_text = pattern.sub(url_func, new_text)
1556 1558 log.debug('processed prefix:uid `%s`', uid)
1557 1559
1558 1560 # finally use global replace, eg !123 -> pr-link, those will not catch
1559 1561 # if already similar pattern exists
1560 1562 server_url = '${scheme}://${netloc}'
1561 1563 pr_entry = {
1562 1564 'pref': '!',
1563 1565 'url': server_url + '/_admin/pull-requests/${id}',
1564 1566 'desc': 'Pull Request !${id}',
1565 1567 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1566 1568 }
1567 1569 pr_url_func = partial(
1568 1570 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1569 1571 link_format=link_format+'+hovercard')
1570 1572 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1571 1573 log.debug('processed !pr pattern')
1572 1574
1573 1575 return new_text, issues_data
1574 1576
1575 1577
1576 1578 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1577 1579 """
1578 1580 Parses given text message and makes proper links.
1579 1581 issues are linked to given issue-server, and rest is a commit link
1580 1582 """
1581 1583 def escaper(_text):
1582 1584 return _text.replace('<', '&lt;').replace('>', '&gt;')
1583 1585
1584 1586 new_text = escaper(commit_text)
1585 1587
1586 1588 # extract http/https links and make them real urls
1587 1589 new_text = urlify_text(new_text, safe=False)
1588 1590
1589 1591 # urlify commits - extract commit ids and make link out of them, if we have
1590 1592 # the scope of repository present.
1591 1593 if repository:
1592 1594 new_text = urlify_commits(new_text, repository)
1593 1595
1594 1596 # process issue tracker patterns
1595 1597 new_text, issues = process_patterns(new_text, repository or '',
1596 1598 active_entries=active_pattern_entries)
1597 1599
1598 1600 return literal(new_text)
1599 1601
1600 1602
1601 1603 def render_binary(repo_name, file_obj):
1602 1604 """
1603 1605 Choose how to render a binary file
1604 1606 """
1605 1607
1606 1608 filename = file_obj.name
1607 1609
1608 1610 # images
1609 1611 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1610 1612 if fnmatch.fnmatch(filename, pat=ext):
1611 1613 alt = escape(filename)
1612 1614 src = route_path(
1613 1615 'repo_file_raw', repo_name=repo_name,
1614 1616 commit_id=file_obj.commit.raw_id,
1615 1617 f_path=file_obj.path)
1616 1618 return literal(
1617 1619 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1618 1620
1619 1621
1620 1622 def renderer_from_filename(filename, exclude=None):
1621 1623 """
1622 1624 choose a renderer based on filename, this works only for text based files
1623 1625 """
1624 1626
1625 1627 # ipython
1626 1628 for ext in ['*.ipynb']:
1627 1629 if fnmatch.fnmatch(filename, pat=ext):
1628 1630 return 'jupyter'
1629 1631
1630 1632 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1631 1633 if is_markup:
1632 1634 return is_markup
1633 1635 return None
1634 1636
1635 1637
1636 1638 def render(source, renderer='rst', mentions=False, relative_urls=None,
1637 1639 repo_name=None):
1638 1640
1639 1641 def maybe_convert_relative_links(html_source):
1640 1642 if relative_urls:
1641 1643 return relative_links(html_source, relative_urls)
1642 1644 return html_source
1643 1645
1644 1646 if renderer == 'plain':
1645 1647 return literal(
1646 1648 MarkupRenderer.plain(source, leading_newline=False))
1647 1649
1648 1650 elif renderer == 'rst':
1649 1651 if repo_name:
1650 1652 # process patterns on comments if we pass in repo name
1651 1653 source, issues = process_patterns(
1652 1654 source, repo_name, link_format='rst')
1653 1655
1654 1656 return literal(
1655 1657 '<div class="rst-block">%s</div>' %
1656 1658 maybe_convert_relative_links(
1657 1659 MarkupRenderer.rst(source, mentions=mentions)))
1658 1660
1659 1661 elif renderer == 'markdown':
1660 1662 if repo_name:
1661 1663 # process patterns on comments if we pass in repo name
1662 1664 source, issues = process_patterns(
1663 1665 source, repo_name, link_format='markdown')
1664 1666
1665 1667 return literal(
1666 1668 '<div class="markdown-block">%s</div>' %
1667 1669 maybe_convert_relative_links(
1668 1670 MarkupRenderer.markdown(source, flavored=True,
1669 1671 mentions=mentions)))
1670 1672
1671 1673 elif renderer == 'jupyter':
1672 1674 return literal(
1673 1675 '<div class="ipynb">%s</div>' %
1674 1676 maybe_convert_relative_links(
1675 1677 MarkupRenderer.jupyter(source)))
1676 1678
1677 1679 # None means just show the file-source
1678 1680 return None
1679 1681
1680 1682
1681 1683 def commit_status(repo, commit_id):
1682 1684 return ChangesetStatusModel().get_status(repo, commit_id)
1683 1685
1684 1686
1685 1687 def commit_status_lbl(commit_status):
1686 1688 return dict(ChangesetStatus.STATUSES).get(commit_status)
1687 1689
1688 1690
1689 1691 def commit_time(repo_name, commit_id):
1690 1692 repo = Repository.get_by_repo_name(repo_name)
1691 1693 commit = repo.get_commit(commit_id=commit_id)
1692 1694 return commit.date
1693 1695
1694 1696
1695 1697 def get_permission_name(key):
1696 1698 return dict(Permission.PERMS).get(key)
1697 1699
1698 1700
1699 1701 def journal_filter_help(request):
1700 1702 _ = request.translate
1701 1703 from rhodecode.lib.audit_logger import ACTIONS
1702 1704 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1703 1705
1704 1706 return _(
1705 1707 'Example filter terms:\n' +
1706 1708 ' repository:vcs\n' +
1707 1709 ' username:marcin\n' +
1708 1710 ' username:(NOT marcin)\n' +
1709 1711 ' action:*push*\n' +
1710 1712 ' ip:127.0.0.1\n' +
1711 1713 ' date:20120101\n' +
1712 1714 ' date:[20120101100000 TO 20120102]\n' +
1713 1715 '\n' +
1714 1716 'Actions: {actions}\n' +
1715 1717 '\n' +
1716 1718 'Generate wildcards using \'*\' character:\n' +
1717 1719 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1718 1720 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1719 1721 '\n' +
1720 1722 'Optional AND / OR operators in queries\n' +
1721 1723 ' "repository:vcs OR repository:test"\n' +
1722 1724 ' "username:test AND repository:test*"\n'
1723 1725 ).format(actions=actions)
1724 1726
1725 1727
1726 1728 def not_mapped_error(repo_name):
1727 1729 from rhodecode.translation import _
1728 1730 flash(_('%s repository is not mapped to db perhaps'
1729 1731 ' it was created or renamed from the filesystem'
1730 1732 ' please run the application again'
1731 1733 ' in order to rescan repositories') % repo_name, category='error')
1732 1734
1733 1735
1734 1736 def ip_range(ip_addr):
1735 1737 from rhodecode.model.db import UserIpMap
1736 1738 s, e = UserIpMap._get_ip_range(ip_addr)
1737 1739 return '%s - %s' % (s, e)
1738 1740
1739 1741
1740 1742 def form(url, method='post', needs_csrf_token=True, **attrs):
1741 1743 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1742 1744 if method.lower() != 'get' and needs_csrf_token:
1743 1745 raise Exception(
1744 1746 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1745 1747 'CSRF token. If the endpoint does not require such token you can ' +
1746 1748 'explicitly set the parameter needs_csrf_token to false.')
1747 1749
1748 1750 return insecure_form(url, method=method, **attrs)
1749 1751
1750 1752
1751 1753 def secure_form(form_url, method="POST", multipart=False, **attrs):
1752 1754 """Start a form tag that points the action to an url. This
1753 1755 form tag will also include the hidden field containing
1754 1756 the auth token.
1755 1757
1756 1758 The url options should be given either as a string, or as a
1757 1759 ``url()`` function. The method for the form defaults to POST.
1758 1760
1759 1761 Options:
1760 1762
1761 1763 ``multipart``
1762 1764 If set to True, the enctype is set to "multipart/form-data".
1763 1765 ``method``
1764 1766 The method to use when submitting the form, usually either
1765 1767 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1766 1768 hidden input with name _method is added to simulate the verb
1767 1769 over POST.
1768 1770
1769 1771 """
1770 1772
1771 1773 if 'request' in attrs:
1772 1774 session = attrs['request'].session
1773 1775 del attrs['request']
1774 1776 else:
1775 1777 raise ValueError(
1776 1778 'Calling this form requires request= to be passed as argument')
1777 1779
1778 1780 _form = insecure_form(form_url, method, multipart, **attrs)
1779 1781 token = literal(
1780 1782 '<input type="hidden" name="{}" value="{}">'.format(
1781 1783 csrf_token_key, get_csrf_token(session)))
1782 1784
1783 1785 return literal("%s\n%s" % (_form, token))
1784 1786
1785 1787
1786 1788 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1787 1789 select_html = select(name, selected, options, **attrs)
1788 1790
1789 1791 select2 = """
1790 1792 <script>
1791 1793 $(document).ready(function() {
1792 1794 $('#%s').select2({
1793 1795 containerCssClass: 'drop-menu %s',
1794 1796 dropdownCssClass: 'drop-menu-dropdown',
1795 1797 dropdownAutoWidth: true%s
1796 1798 });
1797 1799 });
1798 1800 </script>
1799 1801 """
1800 1802
1801 1803 filter_option = """,
1802 1804 minimumResultsForSearch: -1
1803 1805 """
1804 1806 input_id = attrs.get('id') or name
1805 1807 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1806 1808 filter_enabled = "" if enable_filter else filter_option
1807 1809 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1808 1810
1809 1811 return literal(select_html+select_script)
1810 1812
1811 1813
1812 1814 def get_visual_attr(tmpl_context_var, attr_name):
1813 1815 """
1814 1816 A safe way to get a variable from visual variable of template context
1815 1817
1816 1818 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1817 1819 :param attr_name: name of the attribute we fetch from the c.visual
1818 1820 """
1819 1821 visual = getattr(tmpl_context_var, 'visual', None)
1820 1822 if not visual:
1821 1823 return
1822 1824 else:
1823 1825 return getattr(visual, attr_name, None)
1824 1826
1825 1827
1826 1828 def get_last_path_part(file_node):
1827 1829 if not file_node.path:
1828 1830 return u'/'
1829 1831
1830 1832 path = safe_unicode(file_node.path.split('/')[-1])
1831 1833 return u'../' + path
1832 1834
1833 1835
1834 1836 def route_url(*args, **kwargs):
1835 1837 """
1836 1838 Wrapper around pyramids `route_url` (fully qualified url) function.
1837 1839 """
1838 1840 req = get_current_request()
1839 1841 return req.route_url(*args, **kwargs)
1840 1842
1841 1843
1842 1844 def route_path(*args, **kwargs):
1843 1845 """
1844 1846 Wrapper around pyramids `route_path` function.
1845 1847 """
1846 1848 req = get_current_request()
1847 1849 return req.route_path(*args, **kwargs)
1848 1850
1849 1851
1850 1852 def route_path_or_none(*args, **kwargs):
1851 1853 try:
1852 1854 return route_path(*args, **kwargs)
1853 1855 except KeyError:
1854 1856 return None
1855 1857
1856 1858
1857 1859 def current_route_path(request, **kw):
1858 1860 new_args = request.GET.mixed()
1859 1861 new_args.update(kw)
1860 1862 return request.current_route_path(_query=new_args)
1861 1863
1862 1864
1863 1865 def curl_api_example(method, args):
1864 1866 args_json = json.dumps(OrderedDict([
1865 1867 ('id', 1),
1866 1868 ('auth_token', 'SECRET'),
1867 1869 ('method', method),
1868 1870 ('args', args)
1869 1871 ]))
1870 1872
1871 1873 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1872 1874 api_url=route_url('apiv2'),
1873 1875 args_json=args_json
1874 1876 )
1875 1877
1876 1878
1877 1879 def api_call_example(method, args):
1878 1880 """
1879 1881 Generates an API call example via CURL
1880 1882 """
1881 1883 curl_call = curl_api_example(method, args)
1882 1884
1883 1885 return literal(
1884 1886 curl_call +
1885 1887 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1886 1888 "and needs to be of `api calls` role."
1887 1889 .format(token_url=route_url('my_account_auth_tokens')))
1888 1890
1889 1891
1890 1892 def notification_description(notification, request):
1891 1893 """
1892 1894 Generate notification human readable description based on notification type
1893 1895 """
1894 1896 from rhodecode.model.notification import NotificationModel
1895 1897 return NotificationModel().make_description(
1896 1898 notification, translate=request.translate)
1897 1899
1898 1900
1899 1901 def go_import_header(request, db_repo=None):
1900 1902 """
1901 1903 Creates a header for go-import functionality in Go Lang
1902 1904 """
1903 1905
1904 1906 if not db_repo:
1905 1907 return
1906 1908 if 'go-get' not in request.GET:
1907 1909 return
1908 1910
1909 1911 clone_url = db_repo.clone_url()
1910 1912 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1911 1913 # we have a repo and go-get flag,
1912 1914 return literal('<meta name="go-import" content="{} {} {}">'.format(
1913 1915 prefix, db_repo.repo_type, clone_url))
1914 1916
1915 1917
1916 1918 def reviewer_as_json(*args, **kwargs):
1917 1919 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1918 1920 return _reviewer_as_json(*args, **kwargs)
1919 1921
1920 1922
1921 1923 def get_repo_view_type(request):
1922 1924 route_name = request.matched_route.name
1923 1925 route_to_view_type = {
1924 1926 'repo_changelog': 'commits',
1925 1927 'repo_commits': 'commits',
1926 1928 'repo_files': 'files',
1927 1929 'repo_summary': 'summary',
1928 1930 'repo_commit': 'commit'
1929 1931 }
1930 1932
1931 1933 return route_to_view_type.get(route_name)
1932 1934
1933 1935
1934 1936 def is_active(menu_entry, selected):
1935 1937 """
1936 1938 Returns active class for selecting menus in templates
1937 1939 <li class=${h.is_active('settings', current_active)}></li>
1938 1940 """
1939 1941 if not isinstance(menu_entry, list):
1940 1942 menu_entry = [menu_entry]
1941 1943
1942 1944 if selected in menu_entry:
1943 1945 return "active"
@@ -1,2937 +1,2985 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
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 font-weight: @text-semibold-weight;
43 43 font-family: @text-semibold;
44 44 }
45 45
46 46 html {
47 47 display: table;
48 48 height: 100%;
49 49 width: 100%;
50 50 }
51 51
52 52 body {
53 53 display: table-cell;
54 54 width: 100%;
55 55 }
56 56
57 57 //--- LAYOUT ------------------//
58 58
59 59 .hidden{
60 60 display: none !important;
61 61 }
62 62
63 63 .box{
64 64 float: left;
65 65 width: 100%;
66 66 }
67 67
68 68 .browser-header {
69 69 clear: both;
70 70 }
71 71 .main {
72 72 clear: both;
73 73 padding:0 0 @pagepadding;
74 74 height: auto;
75 75
76 76 &:after { //clearfix
77 77 content:"";
78 78 clear:both;
79 79 width:100%;
80 80 display:block;
81 81 }
82 82 }
83 83
84 84 .action-link{
85 85 margin-left: @padding;
86 86 padding-left: @padding;
87 87 border-left: @border-thickness solid @border-default-color;
88 88 }
89 89
90 90 input + .action-link, .action-link.first{
91 91 border-left: none;
92 92 }
93 93
94 94 .action-link.last{
95 95 margin-right: @padding;
96 96 padding-right: @padding;
97 97 }
98 98
99 99 .action-link.active,
100 100 .action-link.active a{
101 101 color: @grey4;
102 102 }
103 103
104 104 .action-link.disabled {
105 105 color: @grey4;
106 106 cursor: inherit;
107 107 }
108 108
109 109
110 110 .clipboard-action {
111 111 cursor: pointer;
112 112 margin-left: 5px;
113 113
114 114 &:not(.no-grey) {
115 115
116 116 &:hover {
117 117 color: @grey2;
118 118 }
119 119 color: @grey4;
120 120 }
121 121 }
122 122
123 123 ul.simple-list{
124 124 list-style: none;
125 125 margin: 0;
126 126 padding: 0;
127 127 }
128 128
129 129 .main-content {
130 130 padding-bottom: @pagepadding;
131 131 }
132 132
133 133 .wide-mode-wrapper {
134 134 max-width:4000px !important;
135 135 }
136 136
137 137 .wrapper {
138 138 position: relative;
139 139 max-width: @wrapper-maxwidth;
140 140 margin: 0 auto;
141 141 }
142 142
143 143 #content {
144 144 clear: both;
145 145 padding: 0 @contentpadding;
146 146 }
147 147
148 148 .advanced-settings-fields{
149 149 input{
150 150 margin-left: @textmargin;
151 151 margin-right: @padding/2;
152 152 }
153 153 }
154 154
155 155 .cs_files_title {
156 156 margin: @pagepadding 0 0;
157 157 }
158 158
159 159 input.inline[type="file"] {
160 160 display: inline;
161 161 }
162 162
163 163 .error_page {
164 164 margin: 10% auto;
165 165
166 166 h1 {
167 167 color: @grey2;
168 168 }
169 169
170 170 .alert {
171 171 margin: @padding 0;
172 172 }
173 173
174 174 .error-branding {
175 175 color: @grey4;
176 176 font-weight: @text-semibold-weight;
177 177 font-family: @text-semibold;
178 178 }
179 179
180 180 .error_message {
181 181 font-family: @text-regular;
182 182 }
183 183
184 184 .sidebar {
185 185 min-height: 275px;
186 186 margin: 0;
187 187 padding: 0 0 @sidebarpadding @sidebarpadding;
188 188 border: none;
189 189 }
190 190
191 191 .main-content {
192 192 position: relative;
193 193 margin: 0 @sidebarpadding @sidebarpadding;
194 194 padding: 0 0 0 @sidebarpadding;
195 195 border-left: @border-thickness solid @grey5;
196 196
197 197 @media (max-width:767px) {
198 198 clear: both;
199 199 width: 100%;
200 200 margin: 0;
201 201 border: none;
202 202 }
203 203 }
204 204
205 205 .inner-column {
206 206 float: left;
207 207 width: 29.75%;
208 208 min-height: 150px;
209 209 margin: @sidebarpadding 2% 0 0;
210 210 padding: 0 2% 0 0;
211 211 border-right: @border-thickness solid @grey5;
212 212
213 213 @media (max-width:767px) {
214 214 clear: both;
215 215 width: 100%;
216 216 border: none;
217 217 }
218 218
219 219 ul {
220 220 padding-left: 1.25em;
221 221 }
222 222
223 223 &:last-child {
224 224 margin: @sidebarpadding 0 0;
225 225 border: none;
226 226 }
227 227
228 228 h4 {
229 229 margin: 0 0 @padding;
230 230 font-weight: @text-semibold-weight;
231 231 font-family: @text-semibold;
232 232 }
233 233 }
234 234 }
235 235 .error-page-logo {
236 236 width: 130px;
237 237 height: 160px;
238 238 }
239 239
240 240 // HEADER
241 241 .header {
242 242
243 243 // TODO: johbo: Fix login pages, so that they work without a min-height
244 244 // for the header and then remove the min-height. I chose a smaller value
245 245 // intentionally here to avoid rendering issues in the main navigation.
246 246 min-height: 49px;
247 247 min-width: 1024px;
248 248
249 249 position: relative;
250 250 vertical-align: bottom;
251 251 padding: 0 @header-padding;
252 252 background-color: @grey1;
253 253 color: @grey5;
254 254
255 255 .title {
256 256 overflow: visible;
257 257 }
258 258
259 259 &:before,
260 260 &:after {
261 261 content: "";
262 262 clear: both;
263 263 width: 100%;
264 264 }
265 265
266 266 // TODO: johbo: Avoids breaking "Repositories" chooser
267 267 .select2-container .select2-choice .select2-arrow {
268 268 display: none;
269 269 }
270 270 }
271 271
272 272 #header-inner {
273 273 &.title {
274 274 margin: 0;
275 275 }
276 276 &:before,
277 277 &:after {
278 278 content: "";
279 279 clear: both;
280 280 }
281 281 }
282 282
283 283 // Gists
284 284 #files_data {
285 285 clear: both; //for firefox
286 286 padding-top: 10px;
287 287 }
288 288
289 289 #gistid {
290 290 margin-right: @padding;
291 291 }
292 292
293 293 // Global Settings Editor
294 294 .textarea.editor {
295 295 float: left;
296 296 position: relative;
297 297 max-width: @texteditor-width;
298 298
299 299 select {
300 300 position: absolute;
301 301 top:10px;
302 302 right:0;
303 303 }
304 304
305 305 .CodeMirror {
306 306 margin: 0;
307 307 }
308 308
309 309 .help-block {
310 310 margin: 0 0 @padding;
311 311 padding:.5em;
312 312 background-color: @grey6;
313 313 &.pre-formatting {
314 314 white-space: pre;
315 315 }
316 316 }
317 317 }
318 318
319 319 ul.auth_plugins {
320 320 margin: @padding 0 @padding @legend-width;
321 321 padding: 0;
322 322
323 323 li {
324 324 margin-bottom: @padding;
325 325 line-height: 1em;
326 326 list-style-type: none;
327 327
328 328 .auth_buttons .btn {
329 329 margin-right: @padding;
330 330 }
331 331
332 332 }
333 333 }
334 334
335 335
336 336 // My Account PR list
337 337
338 338 #show_closed {
339 339 margin: 0 1em 0 0;
340 340 }
341 341
342 342 #pull_request_list_table {
343 343 .closed {
344 344 background-color: @grey6;
345 345 }
346 346
347 347 .state-creating,
348 348 .state-updating,
349 349 .state-merging
350 350 {
351 351 background-color: @grey6;
352 352 }
353 353
354 354 .td-status {
355 355 padding-left: .5em;
356 356 }
357 357 .log-container .truncate {
358 358 height: 2.75em;
359 359 white-space: pre-line;
360 360 }
361 361 table.rctable .user {
362 362 padding-left: 0;
363 363 }
364 364 table.rctable {
365 365 td.td-description,
366 366 .rc-user {
367 367 min-width: auto;
368 368 }
369 369 }
370 370 }
371 371
372 372 // Pull Requests
373 373
374 374 .pullrequests_section_head {
375 375 display: block;
376 376 clear: both;
377 377 margin: @padding 0;
378 378 font-weight: @text-bold-weight;
379 379 font-family: @text-bold;
380 380 }
381 381
382 .pr-origininfo, .pr-targetinfo {
382 .pr-commit-flow {
383 383 position: relative;
384 font-weight: 600;
384 385
385 386 .tag {
386 387 display: inline-block;
387 388 margin: 0 1em .5em 0;
388 389 }
389 390
390 391 .clone-url {
391 392 display: inline-block;
392 393 margin: 0 0 .5em 0;
393 394 padding: 0;
394 395 line-height: 1.2em;
395 396 }
396 397 }
397 398
398 399 .pr-mergeinfo {
399 400 min-width: 95% !important;
400 401 padding: 0 !important;
401 402 border: 0;
402 403 }
403 404 .pr-mergeinfo-copy {
404 405 padding: 0 0;
405 406 }
406 407
407 408 .pr-pullinfo {
408 409 min-width: 95% !important;
409 410 padding: 0 !important;
410 411 border: 0;
411 412 }
412 413 .pr-pullinfo-copy {
413 414 padding: 0 0;
414 415 }
415 416
416
417 417 .pr-title-input {
418 418 width: 80%;
419 419 font-size: 1em;
420 420 margin: 0 0 4px 0;
421 421 padding: 0;
422 422 line-height: 1.7em;
423 423 color: @text-color;
424 424 letter-spacing: .02em;
425 425 font-weight: @text-bold-weight;
426 426 font-family: @text-bold;
427 427
428 428 &:hover {
429 429 box-shadow: none;
430 430 }
431 431 }
432 432
433 433 #pr-title {
434 434 input {
435 435 border: 1px transparent;
436 436 color: black;
437 437 opacity: 1
438 438 }
439 439 }
440 440
441 .pr-title-closed-tag {
442 font-size: 16px;
443 }
444
445 #pr-desc {
446 padding: 10px 0;
447
448 .markdown-block {
449 padding: 0;
450 margin-bottom: -30px;
451 }
452 }
453
441 454 #pullrequest_title {
442 455 width: 100%;
443 456 box-sizing: border-box;
444 457 }
445 458
446 459 #pr_open_message {
447 460 border: @border-thickness solid #fff;
448 461 border-radius: @border-radius;
449 462 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
450 463 text-align: left;
451 464 overflow: hidden;
452 465 }
453 466
467 .pr-details-title {
468 height: 16px
469 }
470
471 .pr-details-title-author-pref {
472 padding-right: 10px
473 }
474
475 .label-pr-detail {
476 display: table-cell;
477 width: 120px;
478 padding-top: 7.5px;
479 padding-bottom: 7.5px;
480 padding-right: 7.5px;
481 }
482
483 .source-details ul {
484 padding: 10px 16px;
485 }
486
487 .source-details-action {
488 color: @grey4;
489 font-size: 11px
490 }
491
454 492 .pr-submit-button {
455 493 float: right;
456 494 margin: 0 0 0 5px;
457 495 }
458 496
459 497 .pr-spacing-container {
460 498 padding: 20px;
461 499 clear: both
462 500 }
463 501
464 502 #pr-description-input {
465 503 margin-bottom: 0;
466 504 }
467 505
468 506 .pr-description-label {
469 507 vertical-align: top;
470 508 }
471 509
510 #close_edit_pullrequest {
511 padding-left: 1em
512 }
513
514 #delete_pullrequest {
515 clear: inherit;
516 padding: 0
517 }
518
472 519 .perms_section_head {
473 520 min-width: 625px;
474 521
475 522 h2 {
476 523 margin-bottom: 0;
477 524 }
478 525
479 526 .label-checkbox {
480 527 float: left;
481 528 }
482 529
483 530 &.field {
484 531 margin: @space 0 @padding;
485 532 }
486 533
487 534 &:first-child.field {
488 535 margin-top: 0;
489 536
490 537 .label {
491 538 margin-top: 0;
492 539 padding-top: 0;
493 540 }
494 541
495 542 .radios {
496 543 padding-top: 0;
497 544 }
498 545 }
499 546
500 547 .radios {
501 548 position: relative;
502 549 width: 505px;
503 550 }
504 551 }
505 552
506 553 //--- MODULES ------------------//
507 554
508 555
509 556 // Server Announcement
510 557 #server-announcement {
511 558 width: 95%;
512 559 margin: @padding auto;
513 560 padding: @padding;
514 561 border-width: 2px;
515 562 border-style: solid;
516 563 .border-radius(2px);
517 564 font-weight: @text-bold-weight;
518 565 font-family: @text-bold;
519 566
520 567 &.info { border-color: @alert4; background-color: @alert4-inner; }
521 568 &.warning { border-color: @alert3; background-color: @alert3-inner; }
522 569 &.error { border-color: @alert2; background-color: @alert2-inner; }
523 570 &.success { border-color: @alert1; background-color: @alert1-inner; }
524 571 &.neutral { border-color: @grey3; background-color: @grey6; }
525 572 }
526 573
527 574 // Fixed Sidebar Column
528 575 .sidebar-col-wrapper {
529 576 padding-left: @sidebar-all-width;
530 577
531 578 .sidebar {
532 579 width: @sidebar-width;
533 580 margin-left: -@sidebar-all-width;
534 581 }
535 582 }
536 583
537 584 .sidebar-col-wrapper.scw-small {
538 585 padding-left: @sidebar-small-all-width;
539 586
540 587 .sidebar {
541 588 width: @sidebar-small-width;
542 589 margin-left: -@sidebar-small-all-width;
543 590 }
544 591 }
545 592
546 593
547 594 // FOOTER
548 595 #footer {
549 596 padding: 0;
550 597 text-align: center;
551 598 vertical-align: middle;
552 599 color: @grey2;
553 600 font-size: 11px;
554 601
555 602 p {
556 603 margin: 0;
557 604 padding: 1em;
558 605 line-height: 1em;
559 606 }
560 607
561 608 .server-instance { //server instance
562 609 display: none;
563 610 }
564 611
565 612 .title {
566 613 float: none;
567 614 margin: 0 auto;
568 615 }
569 616 }
570 617
571 618 button.close {
572 619 padding: 0;
573 620 cursor: pointer;
574 621 background: transparent;
575 622 border: 0;
576 623 .box-shadow(none);
577 624 -webkit-appearance: none;
578 625 }
579 626
580 627 .close {
581 628 float: right;
582 629 font-size: 21px;
583 630 font-family: @text-bootstrap;
584 631 line-height: 1em;
585 632 font-weight: bold;
586 633 color: @grey2;
587 634
588 635 &:hover,
589 636 &:focus {
590 637 color: @grey1;
591 638 text-decoration: none;
592 639 cursor: pointer;
593 640 }
594 641 }
595 642
596 643 // GRID
597 644 .sorting,
598 645 .sorting_desc,
599 646 .sorting_asc {
600 647 cursor: pointer;
601 648 }
602 649 .sorting_desc:after {
603 650 content: "\00A0\25B2";
604 651 font-size: .75em;
605 652 }
606 653 .sorting_asc:after {
607 654 content: "\00A0\25BC";
608 655 font-size: .68em;
609 656 }
610 657
611 658
612 659 .user_auth_tokens {
613 660
614 661 &.truncate {
615 662 white-space: nowrap;
616 663 overflow: hidden;
617 664 text-overflow: ellipsis;
618 665 }
619 666
620 667 .fields .field .input {
621 668 margin: 0;
622 669 }
623 670
624 671 input#description {
625 672 width: 100px;
626 673 margin: 0;
627 674 }
628 675
629 676 .drop-menu {
630 677 // TODO: johbo: Remove this, should work out of the box when
631 678 // having multiple inputs inline
632 679 margin: 0 0 0 5px;
633 680 }
634 681 }
635 682 #user_list_table {
636 683 .closed {
637 684 background-color: @grey6;
638 685 }
639 686 }
640 687
641 688
642 689 input, textarea {
643 690 &.disabled {
644 691 opacity: .5;
645 692 }
646 693
647 694 &:hover {
648 695 border-color: @grey3;
649 696 box-shadow: @button-shadow;
650 697 }
651 698
652 699 &:focus {
653 700 border-color: @rcblue;
654 701 box-shadow: @button-shadow;
655 702 }
656 703 }
657 704
658 705 // remove extra padding in firefox
659 706 input::-moz-focus-inner { border:0; padding:0 }
660 707
661 708 .adjacent input {
662 709 margin-bottom: @padding;
663 710 }
664 711
665 712 .permissions_boxes {
666 713 display: block;
667 714 }
668 715
669 716 //FORMS
670 717
671 718 .medium-inline,
672 719 input#description.medium-inline {
673 720 display: inline;
674 721 width: @medium-inline-input-width;
675 722 min-width: 100px;
676 723 }
677 724
678 725 select {
679 726 //reset
680 727 -webkit-appearance: none;
681 728 -moz-appearance: none;
682 729
683 730 display: inline-block;
684 731 height: 28px;
685 732 width: auto;
686 733 margin: 0 @padding @padding 0;
687 734 padding: 0 18px 0 8px;
688 735 line-height:1em;
689 736 font-size: @basefontsize;
690 737 border: @border-thickness solid @grey5;
691 738 border-radius: @border-radius;
692 739 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
693 740 color: @grey4;
694 741 box-shadow: @button-shadow;
695 742
696 743 &:after {
697 744 content: "\00A0\25BE";
698 745 }
699 746
700 747 &:focus, &:hover {
701 748 outline: none;
702 749 border-color: @grey4;
703 750 color: @rcdarkblue;
704 751 }
705 752 }
706 753
707 754 option {
708 755 &:focus {
709 756 outline: none;
710 757 }
711 758 }
712 759
713 760 input,
714 761 textarea {
715 762 padding: @input-padding;
716 763 border: @input-border-thickness solid @border-highlight-color;
717 764 .border-radius (@border-radius);
718 765 font-family: @text-light;
719 766 font-size: @basefontsize;
720 767
721 768 &.input-sm {
722 769 padding: 5px;
723 770 }
724 771
725 772 &#description {
726 773 min-width: @input-description-minwidth;
727 774 min-height: 1em;
728 775 padding: 10px;
729 776 }
730 777 }
731 778
732 779 .field-sm {
733 780 input,
734 781 textarea {
735 782 padding: 5px;
736 783 }
737 784 }
738 785
739 786 textarea {
740 787 display: block;
741 788 clear: both;
742 789 width: 100%;
743 790 min-height: 100px;
744 791 margin-bottom: @padding;
745 792 .box-sizing(border-box);
746 793 overflow: auto;
747 794 }
748 795
749 796 label {
750 797 font-family: @text-light;
751 798 }
752 799
753 800 // GRAVATARS
754 801 // centers gravatar on username to the right
755 802
756 803 .gravatar {
757 804 display: inline;
758 805 min-width: 16px;
759 806 min-height: 16px;
760 807 margin: -5px 0;
761 808 padding: 0;
762 809 line-height: 1em;
763 810 box-sizing: content-box;
764 811 border-radius: 50%;
765 812
766 813 &.gravatar-large {
767 814 margin: -0.5em .25em -0.5em 0;
768 815 }
769 816
770 817 & + .user {
771 818 display: inline;
772 819 margin: 0;
773 820 padding: 0 0 0 .17em;
774 821 line-height: 1em;
775 822 }
776 823 }
777 824
778 825 .user-inline-data {
779 826 display: inline-block;
780 827 float: left;
781 828 padding-left: .5em;
782 829 line-height: 1.3em;
783 830 }
784 831
785 832 .rc-user { // gravatar + user wrapper
786 833 float: left;
787 834 position: relative;
788 835 min-width: 100px;
789 836 max-width: 200px;
790 837 min-height: (@gravatar-size + @border-thickness * 2); // account for border
791 838 display: block;
792 839 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
793 840
794 841
795 842 .gravatar {
796 843 display: block;
797 844 position: absolute;
798 845 top: 0;
799 846 left: 0;
800 847 min-width: @gravatar-size;
801 848 min-height: @gravatar-size;
802 849 margin: 0;
803 850 }
804 851
805 852 .user {
806 853 display: block;
807 854 max-width: 175px;
808 855 padding-top: 2px;
809 856 overflow: hidden;
810 857 text-overflow: ellipsis;
811 858 }
812 859 }
813 860
814 861 .gist-gravatar,
815 862 .journal_container {
816 863 .gravatar-large {
817 864 margin: 0 .5em -10px 0;
818 865 }
819 866 }
820 867
821 868 .gist-type-fields {
822 869 line-height: 30px;
823 870 height: 30px;
824 871
825 872 .gist-type-fields-wrapper {
826 873 vertical-align: middle;
827 874 display: inline-block;
828 875 line-height: 25px;
829 876 }
830 877 }
831 878
832 879 // ADMIN SETTINGS
833 880
834 881 // Tag Patterns
835 882 .tag_patterns {
836 883 .tag_input {
837 884 margin-bottom: @padding;
838 885 }
839 886 }
840 887
841 888 .locked_input {
842 889 position: relative;
843 890
844 891 input {
845 892 display: inline;
846 893 margin: 3px 5px 0px 0px;
847 894 }
848 895
849 896 br {
850 897 display: none;
851 898 }
852 899
853 900 .error-message {
854 901 float: left;
855 902 width: 100%;
856 903 }
857 904
858 905 .lock_input_button {
859 906 display: inline;
860 907 }
861 908
862 909 .help-block {
863 910 clear: both;
864 911 }
865 912 }
866 913
867 914 // Notifications
868 915
869 916 .notifications_buttons {
870 917 margin: 0 0 @space 0;
871 918 padding: 0;
872 919
873 920 .btn {
874 921 display: inline-block;
875 922 }
876 923 }
877 924
878 925 .notification-list {
879 926
880 927 div {
881 928 vertical-align: middle;
882 929 }
883 930
884 931 .container {
885 932 display: block;
886 933 margin: 0 0 @padding 0;
887 934 }
888 935
889 936 .delete-notifications {
890 937 margin-left: @padding;
891 938 text-align: right;
892 939 cursor: pointer;
893 940 }
894 941
895 942 .read-notifications {
896 943 margin-left: @padding/2;
897 944 text-align: right;
898 945 width: 35px;
899 946 cursor: pointer;
900 947 }
901 948
902 949 .icon-minus-sign {
903 950 color: @alert2;
904 951 }
905 952
906 953 .icon-ok-sign {
907 954 color: @alert1;
908 955 }
909 956 }
910 957
911 958 .user_settings {
912 959 float: left;
913 960 clear: both;
914 961 display: block;
915 962 width: 100%;
916 963
917 964 .gravatar_box {
918 965 margin-bottom: @padding;
919 966
920 967 &:after {
921 968 content: " ";
922 969 clear: both;
923 970 width: 100%;
924 971 }
925 972 }
926 973
927 974 .fields .field {
928 975 clear: both;
929 976 }
930 977 }
931 978
932 979 .advanced_settings {
933 980 margin-bottom: @space;
934 981
935 982 .help-block {
936 983 margin-left: 0;
937 984 }
938 985
939 986 button + .help-block {
940 987 margin-top: @padding;
941 988 }
942 989 }
943 990
944 991 // admin settings radio buttons and labels
945 992 .label-2 {
946 993 float: left;
947 994 width: @label2-width;
948 995
949 996 label {
950 997 color: @grey1;
951 998 }
952 999 }
953 1000 .checkboxes {
954 1001 float: left;
955 1002 width: @checkboxes-width;
956 1003 margin-bottom: @padding;
957 1004
958 1005 .checkbox {
959 1006 width: 100%;
960 1007
961 1008 label {
962 1009 margin: 0;
963 1010 padding: 0;
964 1011 }
965 1012 }
966 1013
967 1014 .checkbox + .checkbox {
968 1015 display: inline-block;
969 1016 }
970 1017
971 1018 label {
972 1019 margin-right: 1em;
973 1020 }
974 1021 }
975 1022
976 1023 // CHANGELOG
977 1024 .container_header {
978 1025 float: left;
979 1026 display: block;
980 1027 width: 100%;
981 1028 margin: @padding 0 @padding;
982 1029
983 1030 #filter_changelog {
984 1031 float: left;
985 1032 margin-right: @padding;
986 1033 }
987 1034
988 1035 .breadcrumbs_light {
989 1036 display: inline-block;
990 1037 }
991 1038 }
992 1039
993 1040 .info_box {
994 1041 float: right;
995 1042 }
996 1043
997 1044
998 1045
999 1046 #graph_content{
1000 1047
1001 1048 // adjust for table headers so that graph renders properly
1002 1049 // #graph_nodes padding - table cell padding
1003 1050 padding-top: (@space - (@basefontsize * 2.4));
1004 1051
1005 1052 &.graph_full_width {
1006 1053 width: 100%;
1007 1054 max-width: 100%;
1008 1055 }
1009 1056 }
1010 1057
1011 1058 #graph {
1012 1059
1013 1060 .pagination-left {
1014 1061 float: left;
1015 1062 clear: both;
1016 1063 }
1017 1064
1018 1065 .log-container {
1019 1066 max-width: 345px;
1020 1067
1021 1068 .message{
1022 1069 max-width: 340px;
1023 1070 }
1024 1071 }
1025 1072
1026 1073 .graph-col-wrapper {
1027 1074
1028 1075 #graph_nodes {
1029 1076 width: 100px;
1030 1077 position: absolute;
1031 1078 left: 70px;
1032 1079 z-index: -1;
1033 1080 }
1034 1081 }
1035 1082
1036 1083 .load-more-commits {
1037 1084 text-align: center;
1038 1085 }
1039 1086 .load-more-commits:hover {
1040 1087 background-color: @grey7;
1041 1088 }
1042 1089 .load-more-commits {
1043 1090 a {
1044 1091 display: block;
1045 1092 }
1046 1093 }
1047 1094 }
1048 1095
1049 1096 .obsolete-toggle {
1050 1097 line-height: 30px;
1051 1098 margin-left: -15px;
1052 1099 }
1053 1100
1054 1101 #rev_range_container, #rev_range_clear, #rev_range_more {
1055 1102 margin-top: -5px;
1056 1103 margin-bottom: -5px;
1057 1104 }
1058 1105
1059 1106 #filter_changelog {
1060 1107 float: left;
1061 1108 }
1062 1109
1063 1110
1064 1111 //--- THEME ------------------//
1065 1112
1066 1113 #logo {
1067 1114 float: left;
1068 1115 margin: 9px 0 0 0;
1069 1116
1070 1117 .header {
1071 1118 background-color: transparent;
1072 1119 }
1073 1120
1074 1121 a {
1075 1122 display: inline-block;
1076 1123 }
1077 1124
1078 1125 img {
1079 1126 height:30px;
1080 1127 }
1081 1128 }
1082 1129
1083 1130 .logo-wrapper {
1084 1131 float:left;
1085 1132 }
1086 1133
1087 1134 .branding {
1088 1135 float: left;
1089 1136 padding: 9px 2px;
1090 1137 line-height: 1em;
1091 1138 font-size: @navigation-fontsize;
1092 1139
1093 1140 a {
1094 1141 color: @grey5
1095 1142 }
1096 1143 @media screen and (max-width: 1200px) {
1097 1144 display: none;
1098 1145 }
1099 1146 }
1100 1147
1101 1148 img {
1102 1149 border: none;
1103 1150 outline: none;
1104 1151 }
1105 1152 user-profile-header
1106 1153 label {
1107 1154
1108 1155 input[type="checkbox"] {
1109 1156 margin-right: 1em;
1110 1157 }
1111 1158 input[type="radio"] {
1112 1159 margin-right: 1em;
1113 1160 }
1114 1161 }
1115 1162
1116 1163 .review-status {
1117 1164 &.under_review {
1118 1165 color: @alert3;
1119 1166 }
1120 1167 &.approved {
1121 1168 color: @alert1;
1122 1169 }
1123 1170 &.rejected,
1124 1171 &.forced_closed{
1125 1172 color: @alert2;
1126 1173 }
1127 1174 &.not_reviewed {
1128 1175 color: @grey5;
1129 1176 }
1130 1177 }
1131 1178
1132 1179 .review-status-under_review {
1133 1180 color: @alert3;
1134 1181 }
1135 1182 .status-tag-under_review {
1136 1183 border-color: @alert3;
1137 1184 }
1138 1185
1139 1186 .review-status-approved {
1140 1187 color: @alert1;
1141 1188 }
1142 1189 .status-tag-approved {
1143 1190 border-color: @alert1;
1144 1191 }
1145 1192
1146 1193 .review-status-rejected,
1147 1194 .review-status-forced_closed {
1148 1195 color: @alert2;
1149 1196 }
1150 1197 .status-tag-rejected,
1151 1198 .status-tag-forced_closed {
1152 1199 border-color: @alert2;
1153 1200 }
1154 1201
1155 1202 .review-status-not_reviewed {
1156 1203 color: @grey5;
1157 1204 }
1158 1205 .status-tag-not_reviewed {
1159 1206 border-color: @grey5;
1160 1207 }
1161 1208
1162 1209 .test_pattern_preview {
1163 1210 margin: @space 0;
1164 1211
1165 1212 p {
1166 1213 margin-bottom: 0;
1167 1214 border-bottom: @border-thickness solid @border-default-color;
1168 1215 color: @grey3;
1169 1216 }
1170 1217
1171 1218 .btn {
1172 1219 margin-bottom: @padding;
1173 1220 }
1174 1221 }
1175 1222 #test_pattern_result {
1176 1223 display: none;
1177 1224 &:extend(pre);
1178 1225 padding: .9em;
1179 1226 color: @grey3;
1180 1227 background-color: @grey7;
1181 1228 border-right: @border-thickness solid @border-default-color;
1182 1229 border-bottom: @border-thickness solid @border-default-color;
1183 1230 border-left: @border-thickness solid @border-default-color;
1184 1231 }
1185 1232
1186 1233 #repo_vcs_settings {
1187 1234 #inherit_overlay_vcs_default {
1188 1235 display: none;
1189 1236 }
1190 1237 #inherit_overlay_vcs_custom {
1191 1238 display: custom;
1192 1239 }
1193 1240 &.inherited {
1194 1241 #inherit_overlay_vcs_default {
1195 1242 display: block;
1196 1243 }
1197 1244 #inherit_overlay_vcs_custom {
1198 1245 display: none;
1199 1246 }
1200 1247 }
1201 1248 }
1202 1249
1203 1250 .issue-tracker-link {
1204 1251 color: @rcblue;
1205 1252 }
1206 1253
1207 1254 // Issue Tracker Table Show/Hide
1208 1255 #repo_issue_tracker {
1209 1256 #inherit_overlay {
1210 1257 display: none;
1211 1258 }
1212 1259 #custom_overlay {
1213 1260 display: custom;
1214 1261 }
1215 1262 &.inherited {
1216 1263 #inherit_overlay {
1217 1264 display: block;
1218 1265 }
1219 1266 #custom_overlay {
1220 1267 display: none;
1221 1268 }
1222 1269 }
1223 1270 }
1224 1271 table.issuetracker {
1225 1272 &.readonly {
1226 1273 tr, td {
1227 1274 color: @grey3;
1228 1275 }
1229 1276 }
1230 1277 .edit {
1231 1278 display: none;
1232 1279 }
1233 1280 .editopen {
1234 1281 .edit {
1235 1282 display: inline;
1236 1283 }
1237 1284 .entry {
1238 1285 display: none;
1239 1286 }
1240 1287 }
1241 1288 tr td.td-action {
1242 1289 min-width: 117px;
1243 1290 }
1244 1291 td input {
1245 1292 max-width: none;
1246 1293 min-width: 30px;
1247 1294 width: 80%;
1248 1295 }
1249 1296 .issuetracker_pref input {
1250 1297 width: 40%;
1251 1298 }
1252 1299 input.edit_issuetracker_update {
1253 1300 margin-right: 0;
1254 1301 width: auto;
1255 1302 }
1256 1303 }
1257 1304
1258 1305 table.integrations {
1259 1306 .td-icon {
1260 1307 width: 20px;
1261 1308 .integration-icon {
1262 1309 height: 20px;
1263 1310 width: 20px;
1264 1311 }
1265 1312 }
1266 1313 }
1267 1314
1268 1315 .integrations {
1269 1316 a.integration-box {
1270 1317 color: @text-color;
1271 1318 &:hover {
1272 1319 .panel {
1273 1320 background: #fbfbfb;
1274 1321 }
1275 1322 }
1276 1323 .integration-icon {
1277 1324 width: 30px;
1278 1325 height: 30px;
1279 1326 margin-right: 20px;
1280 1327 float: left;
1281 1328 }
1282 1329
1283 1330 .panel-body {
1284 1331 padding: 10px;
1285 1332 }
1286 1333 .panel {
1287 1334 margin-bottom: 10px;
1288 1335 }
1289 1336 h2 {
1290 1337 display: inline-block;
1291 1338 margin: 0;
1292 1339 min-width: 140px;
1293 1340 }
1294 1341 }
1295 1342 a.integration-box.dummy-integration {
1296 1343 color: @grey4
1297 1344 }
1298 1345 }
1299 1346
1300 1347 //Permissions Settings
1301 1348 #add_perm {
1302 1349 margin: 0 0 @padding;
1303 1350 cursor: pointer;
1304 1351 }
1305 1352
1306 1353 .perm_ac {
1307 1354 input {
1308 1355 width: 95%;
1309 1356 }
1310 1357 }
1311 1358
1312 1359 .autocomplete-suggestions {
1313 1360 width: auto !important; // overrides autocomplete.js
1314 1361 min-width: 278px;
1315 1362 margin: 0;
1316 1363 border: @border-thickness solid @grey5;
1317 1364 border-radius: @border-radius;
1318 1365 color: @grey2;
1319 1366 background-color: white;
1320 1367 }
1321 1368
1322 1369 .autocomplete-qfilter-suggestions {
1323 1370 width: auto !important; // overrides autocomplete.js
1324 1371 max-height: 100% !important;
1325 1372 min-width: 376px;
1326 1373 margin: 0;
1327 1374 border: @border-thickness solid @grey5;
1328 1375 color: @grey2;
1329 1376 background-color: white;
1330 1377 }
1331 1378
1332 1379 .autocomplete-selected {
1333 1380 background: #F0F0F0;
1334 1381 }
1335 1382
1336 1383 .ac-container-wrap {
1337 1384 margin: 0;
1338 1385 padding: 8px;
1339 1386 border-bottom: @border-thickness solid @grey5;
1340 1387 list-style-type: none;
1341 1388 cursor: pointer;
1342 1389
1343 1390 &:hover {
1344 1391 background-color: @grey7;
1345 1392 }
1346 1393
1347 1394 img {
1348 1395 height: @gravatar-size;
1349 1396 width: @gravatar-size;
1350 1397 margin-right: 1em;
1351 1398 }
1352 1399
1353 1400 strong {
1354 1401 font-weight: normal;
1355 1402 }
1356 1403 }
1357 1404
1358 1405 // Settings Dropdown
1359 1406 .user-menu .container {
1360 1407 padding: 0 4px;
1361 1408 margin: 0;
1362 1409 }
1363 1410
1364 1411 .user-menu .gravatar {
1365 1412 cursor: pointer;
1366 1413 }
1367 1414
1368 1415 .codeblock {
1369 1416 margin-bottom: @padding;
1370 1417 clear: both;
1371 1418
1372 1419 .stats {
1373 1420 overflow: hidden;
1374 1421 }
1375 1422
1376 1423 .message{
1377 1424 textarea{
1378 1425 margin: 0;
1379 1426 }
1380 1427 }
1381 1428
1382 1429 .code-header {
1383 1430 .stats {
1384 1431 line-height: 2em;
1385 1432
1386 1433 .revision_id {
1387 1434 margin-left: 0;
1388 1435 }
1389 1436 .buttons {
1390 1437 padding-right: 0;
1391 1438 }
1392 1439 }
1393 1440
1394 1441 .item{
1395 1442 margin-right: 0.5em;
1396 1443 }
1397 1444 }
1398 1445
1399 1446 #editor_container {
1400 1447 position: relative;
1401 1448 margin: @padding 10px;
1402 1449 }
1403 1450 }
1404 1451
1405 1452 #file_history_container {
1406 1453 display: none;
1407 1454 }
1408 1455
1409 1456 .file-history-inner {
1410 1457 margin-bottom: 10px;
1411 1458 }
1412 1459
1413 1460 // Pull Requests
1414 1461 .summary-details {
1415 1462 width: 72%;
1416 1463 }
1417 1464 .pr-summary {
1418 1465 border-bottom: @border-thickness solid @grey5;
1419 1466 margin-bottom: @space;
1420 1467 }
1421 1468 .reviewers-title {
1422 1469 width: 25%;
1423 1470 min-width: 200px;
1424 1471 }
1425 1472 .reviewers {
1426 1473 width: 25%;
1427 1474 min-width: 200px;
1428 1475 }
1429 1476 .reviewers ul li {
1430 1477 position: relative;
1431 1478 width: 100%;
1432 1479 padding-bottom: 8px;
1433 1480 list-style-type: none;
1434 1481 }
1435 1482
1436 1483 .reviewer_entry {
1437 1484 min-height: 55px;
1438 1485 }
1439 1486
1440 1487 .reviewers_member {
1441 1488 width: 100%;
1442 1489 overflow: auto;
1443 1490 }
1444 1491 .reviewer_reason {
1445 1492 padding-left: 20px;
1446 1493 line-height: 1.5em;
1447 1494 }
1448 1495 .reviewer_status {
1449 1496 display: inline-block;
1450 1497 width: 25px;
1451 1498 min-width: 25px;
1452 1499 height: 1.2em;
1453 1500 line-height: 1em;
1454 1501 }
1455 1502
1456 1503 .reviewer_name {
1457 1504 display: inline-block;
1458 1505 max-width: 83%;
1459 1506 padding-right: 20px;
1460 1507 vertical-align: middle;
1461 1508 line-height: 1;
1462 1509
1463 1510 .rc-user {
1464 1511 min-width: 0;
1465 1512 margin: -2px 1em 0 0;
1466 1513 }
1467 1514
1468 1515 .reviewer {
1469 1516 float: left;
1470 1517 }
1471 1518 }
1472 1519
1473 1520 .reviewer_member_mandatory {
1474 1521 position: absolute;
1475 1522 left: 15px;
1476 1523 top: 8px;
1477 1524 width: 16px;
1478 1525 font-size: 11px;
1479 1526 margin: 0;
1480 1527 padding: 0;
1481 1528 color: black;
1482 1529 }
1483 1530
1484 1531 .reviewer_member_mandatory_remove,
1485 1532 .reviewer_member_remove {
1486 1533 position: absolute;
1487 1534 right: 0;
1488 1535 top: 0;
1489 1536 width: 16px;
1490 1537 margin-bottom: 10px;
1491 1538 padding: 0;
1492 1539 color: black;
1493 1540 }
1494 1541
1495 1542 .reviewer_member_mandatory_remove {
1496 1543 color: @grey4;
1497 1544 }
1498 1545
1499 1546 .reviewer_member_status {
1500 1547 margin-top: 5px;
1501 1548 }
1502 1549 .pr-summary #summary{
1503 1550 width: 100%;
1504 1551 }
1505 1552 .pr-summary .action_button:hover {
1506 1553 border: 0;
1507 1554 cursor: pointer;
1508 1555 }
1509 1556 .pr-details-title {
1510 1557 padding-bottom: 8px;
1511 1558 border-bottom: @border-thickness solid @grey5;
1512 1559
1513 1560 .action_button.disabled {
1514 1561 color: @grey4;
1515 1562 cursor: inherit;
1516 1563 }
1517 1564 .action_button {
1518 1565 color: @rcblue;
1519 1566 }
1520 1567 }
1521 1568 .pr-details-content {
1522 1569 margin-top: @textmargin;
1523 1570 margin-bottom: @textmargin;
1524 1571 }
1525 1572
1526 1573 .pr-reviewer-rules {
1527 1574 padding: 10px 0px 20px 0px;
1528 1575 }
1529 1576
1530 1577 .group_members {
1531 1578 margin-top: 0;
1532 1579 padding: 0;
1533 1580 list-style: outside none none;
1534 1581
1535 1582 img {
1536 1583 height: @gravatar-size;
1537 1584 width: @gravatar-size;
1538 1585 margin-right: .5em;
1539 1586 margin-left: 3px;
1540 1587 }
1541 1588
1542 1589 .to-delete {
1543 1590 .user {
1544 1591 text-decoration: line-through;
1545 1592 }
1546 1593 }
1547 1594 }
1548 1595
1549 1596 .compare_view_commits_title {
1550 1597 .disabled {
1551 1598 cursor: inherit;
1552 1599 &:hover{
1553 1600 background-color: inherit;
1554 1601 color: inherit;
1555 1602 }
1556 1603 }
1557 1604 }
1558 1605
1559 1606 .subtitle-compare {
1560 1607 margin: -15px 0px 0px 0px;
1561 1608 }
1562 1609
1563 1610 // new entry in group_members
1564 1611 .td-author-new-entry {
1565 1612 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1566 1613 }
1567 1614
1568 1615 .usergroup_member_remove {
1569 1616 width: 16px;
1570 1617 margin-bottom: 10px;
1571 1618 padding: 0;
1572 1619 color: black !important;
1573 1620 cursor: pointer;
1574 1621 }
1575 1622
1576 1623 .reviewer_ac .ac-input {
1577 1624 width: 92%;
1578 1625 margin-bottom: 1em;
1579 1626 }
1580 1627
1581 1628 .compare_view_commits tr{
1582 1629 height: 20px;
1583 1630 }
1584 1631 .compare_view_commits td {
1585 1632 vertical-align: top;
1586 1633 padding-top: 10px;
1587 1634 }
1588 1635 .compare_view_commits .author {
1589 1636 margin-left: 5px;
1590 1637 }
1591 1638
1592 1639 .compare_view_commits {
1593 1640 .color-a {
1594 1641 color: @alert1;
1595 1642 }
1596 1643
1597 1644 .color-c {
1598 1645 color: @color3;
1599 1646 }
1600 1647
1601 1648 .color-r {
1602 1649 color: @color5;
1603 1650 }
1604 1651
1605 1652 .color-a-bg {
1606 1653 background-color: @alert1;
1607 1654 }
1608 1655
1609 1656 .color-c-bg {
1610 1657 background-color: @alert3;
1611 1658 }
1612 1659
1613 1660 .color-r-bg {
1614 1661 background-color: @alert2;
1615 1662 }
1616 1663
1617 1664 .color-a-border {
1618 1665 border: 1px solid @alert1;
1619 1666 }
1620 1667
1621 1668 .color-c-border {
1622 1669 border: 1px solid @alert3;
1623 1670 }
1624 1671
1625 1672 .color-r-border {
1626 1673 border: 1px solid @alert2;
1627 1674 }
1628 1675
1629 1676 .commit-change-indicator {
1630 1677 width: 15px;
1631 1678 height: 15px;
1632 1679 position: relative;
1633 1680 left: 15px;
1634 1681 }
1635 1682
1636 1683 .commit-change-content {
1637 1684 text-align: center;
1638 1685 vertical-align: middle;
1639 1686 line-height: 15px;
1640 1687 }
1641 1688 }
1642 1689
1643 1690 .compare_view_filepath {
1644 1691 color: @grey1;
1645 1692 }
1646 1693
1647 1694 .show_more {
1648 1695 display: inline-block;
1649 1696 width: 0;
1650 1697 height: 0;
1651 1698 vertical-align: middle;
1652 1699 content: "";
1653 1700 border: 4px solid;
1654 1701 border-right-color: transparent;
1655 1702 border-bottom-color: transparent;
1656 1703 border-left-color: transparent;
1657 1704 font-size: 0;
1658 1705 }
1659 1706
1660 1707 .journal_more .show_more {
1661 1708 display: inline;
1662 1709
1663 1710 &:after {
1664 1711 content: none;
1665 1712 }
1666 1713 }
1667 1714
1668 1715 .compare_view_commits .collapse_commit:after {
1669 1716 cursor: pointer;
1670 1717 content: "\00A0\25B4";
1671 1718 margin-left: -3px;
1672 1719 font-size: 17px;
1673 1720 color: @grey4;
1674 1721 }
1675 1722
1676 1723 .diff_links {
1677 1724 margin-left: 8px;
1678 1725 }
1679 1726
1680 1727 #pull_request_overview {
1681 1728 div.ancestor {
1682 1729 margin: -33px 0;
1683 1730 }
1684 1731 }
1685 1732
1686 1733 div.ancestor {
1687 1734 line-height: 33px;
1688 1735 }
1689 1736
1690 1737 .cs_icon_td input[type="checkbox"] {
1691 1738 display: none;
1692 1739 }
1693 1740
1694 1741 .cs_icon_td .expand_file_icon:after {
1695 1742 cursor: pointer;
1696 1743 content: "\00A0\25B6";
1697 1744 font-size: 12px;
1698 1745 color: @grey4;
1699 1746 }
1700 1747
1701 1748 .cs_icon_td .collapse_file_icon:after {
1702 1749 cursor: pointer;
1703 1750 content: "\00A0\25BC";
1704 1751 font-size: 12px;
1705 1752 color: @grey4;
1706 1753 }
1707 1754
1708 1755 /*new binary
1709 1756 NEW_FILENODE = 1
1710 1757 DEL_FILENODE = 2
1711 1758 MOD_FILENODE = 3
1712 1759 RENAMED_FILENODE = 4
1713 1760 COPIED_FILENODE = 5
1714 1761 CHMOD_FILENODE = 6
1715 1762 BIN_FILENODE = 7
1716 1763 */
1717 1764 .cs_files_expand {
1718 1765 font-size: @basefontsize + 5px;
1719 1766 line-height: 1.8em;
1720 1767 float: right;
1721 1768 }
1722 1769
1723 1770 .cs_files_expand span{
1724 1771 color: @rcblue;
1725 1772 cursor: pointer;
1726 1773 }
1727 1774 .cs_files {
1728 1775 clear: both;
1729 1776 padding-bottom: @padding;
1730 1777
1731 1778 .cur_cs {
1732 1779 margin: 10px 2px;
1733 1780 font-weight: bold;
1734 1781 }
1735 1782
1736 1783 .node {
1737 1784 float: left;
1738 1785 }
1739 1786
1740 1787 .changes {
1741 1788 float: right;
1742 1789 color: white;
1743 1790 font-size: @basefontsize - 4px;
1744 1791 margin-top: 4px;
1745 1792 opacity: 0.6;
1746 1793 filter: Alpha(opacity=60); /* IE8 and earlier */
1747 1794
1748 1795 .added {
1749 1796 background-color: @alert1;
1750 1797 float: left;
1751 1798 text-align: center;
1752 1799 }
1753 1800
1754 1801 .deleted {
1755 1802 background-color: @alert2;
1756 1803 float: left;
1757 1804 text-align: center;
1758 1805 }
1759 1806
1760 1807 .bin {
1761 1808 background-color: @alert1;
1762 1809 text-align: center;
1763 1810 }
1764 1811
1765 1812 /*new binary*/
1766 1813 .bin.bin1 {
1767 1814 background-color: @alert1;
1768 1815 text-align: center;
1769 1816 }
1770 1817
1771 1818 /*deleted binary*/
1772 1819 .bin.bin2 {
1773 1820 background-color: @alert2;
1774 1821 text-align: center;
1775 1822 }
1776 1823
1777 1824 /*mod binary*/
1778 1825 .bin.bin3 {
1779 1826 background-color: @grey2;
1780 1827 text-align: center;
1781 1828 }
1782 1829
1783 1830 /*rename file*/
1784 1831 .bin.bin4 {
1785 1832 background-color: @alert4;
1786 1833 text-align: center;
1787 1834 }
1788 1835
1789 1836 /*copied file*/
1790 1837 .bin.bin5 {
1791 1838 background-color: @alert4;
1792 1839 text-align: center;
1793 1840 }
1794 1841
1795 1842 /*chmod file*/
1796 1843 .bin.bin6 {
1797 1844 background-color: @grey2;
1798 1845 text-align: center;
1799 1846 }
1800 1847 }
1801 1848 }
1802 1849
1803 1850 .cs_files .cs_added, .cs_files .cs_A,
1804 1851 .cs_files .cs_added, .cs_files .cs_M,
1805 1852 .cs_files .cs_added, .cs_files .cs_D {
1806 1853 height: 16px;
1807 1854 padding-right: 10px;
1808 1855 margin-top: 7px;
1809 1856 text-align: left;
1810 1857 }
1811 1858
1812 1859 .cs_icon_td {
1813 1860 min-width: 16px;
1814 1861 width: 16px;
1815 1862 }
1816 1863
1817 1864 .pull-request-merge {
1818 1865 border: 1px solid @grey5;
1819 1866 padding: 10px 0px 20px;
1820 1867 margin-top: 10px;
1821 1868 margin-bottom: 20px;
1822 1869 }
1823 1870
1824 1871 .pull-request-merge-refresh {
1825 1872 margin: 2px 7px;
1826 1873 a {
1827 1874 color: @grey3;
1828 1875 }
1829 1876 }
1830 1877
1831 1878 .pull-request-merge ul {
1832 1879 padding: 0px 0px;
1833 1880 }
1834 1881
1835 1882 .pull-request-merge li {
1836 1883 list-style-type: none;
1837 1884 }
1838 1885
1839 1886 .pull-request-merge .pull-request-wrap {
1840 1887 height: auto;
1841 1888 padding: 0px 0px;
1842 1889 text-align: right;
1843 1890 }
1844 1891
1845 1892 .pull-request-merge span {
1846 1893 margin-right: 5px;
1847 1894 }
1848 1895
1849 1896 .pull-request-merge-actions {
1850 1897 min-height: 30px;
1851 1898 padding: 0px 0px;
1852 1899 }
1853 1900
1854 1901 .pull-request-merge-info {
1855 1902 padding: 0px 5px 5px 0px;
1856 1903 }
1857 1904
1858 1905 .merge-status {
1859 1906 margin-right: 5px;
1860 1907 }
1861 1908
1862 1909 .merge-message {
1863 1910 font-size: 1.2em
1864 1911 }
1865 1912
1866 1913 .merge-message.success i,
1867 1914 .merge-icon.success i {
1868 1915 color:@alert1;
1869 1916 }
1870 1917
1871 1918 .merge-message.warning i,
1872 1919 .merge-icon.warning i {
1873 1920 color: @alert3;
1874 1921 }
1875 1922
1876 1923 .merge-message.error i,
1877 1924 .merge-icon.error i {
1878 1925 color:@alert2;
1879 1926 }
1880 1927
1881 1928 .pr-versions {
1882 1929 font-size: 1.1em;
1930 padding: 7.5px;
1883 1931
1884 1932 table {
1885 padding: 0px 5px;
1933
1886 1934 }
1887 1935
1888 1936 td {
1889 1937 line-height: 15px;
1890 1938 }
1891 1939
1892 1940 .compare-radio-button {
1893 1941 position: relative;
1894 1942 top: -3px;
1895 1943 }
1896 1944 }
1897 1945
1898 1946
1899 1947 #close_pull_request {
1900 1948 margin-right: 0px;
1901 1949 }
1902 1950
1903 1951 .empty_data {
1904 1952 color: @grey4;
1905 1953 }
1906 1954
1907 1955 #changeset_compare_view_content {
1908 1956 clear: both;
1909 1957 width: 100%;
1910 1958 box-sizing: border-box;
1911 1959 .border-radius(@border-radius);
1912 1960
1913 1961 .help-block {
1914 1962 margin: @padding 0;
1915 1963 color: @text-color;
1916 1964 &.pre-formatting {
1917 1965 white-space: pre;
1918 1966 }
1919 1967 }
1920 1968
1921 1969 .empty_data {
1922 1970 margin: @padding 0;
1923 1971 }
1924 1972
1925 1973 .alert {
1926 1974 margin-bottom: @space;
1927 1975 }
1928 1976 }
1929 1977
1930 1978 .table_disp {
1931 1979 .status {
1932 1980 width: auto;
1933 1981 }
1934 1982 }
1935 1983
1936 1984
1937 1985 .creation_in_progress {
1938 1986 color: @grey4
1939 1987 }
1940 1988
1941 1989 .status_box_menu {
1942 1990 margin: 0;
1943 1991 }
1944 1992
1945 1993 .notification-table{
1946 1994 margin-bottom: @space;
1947 1995 display: table;
1948 1996 width: 100%;
1949 1997
1950 1998 .container{
1951 1999 display: table-row;
1952 2000
1953 2001 .notification-header{
1954 2002 border-bottom: @border-thickness solid @border-default-color;
1955 2003 }
1956 2004
1957 2005 .notification-subject{
1958 2006 display: table-cell;
1959 2007 }
1960 2008 }
1961 2009 }
1962 2010
1963 2011 // Notifications
1964 2012 .notification-header{
1965 2013 display: table;
1966 2014 width: 100%;
1967 2015 padding: floor(@basefontsize/2) 0;
1968 2016 line-height: 1em;
1969 2017
1970 2018 .desc, .delete-notifications, .read-notifications{
1971 2019 display: table-cell;
1972 2020 text-align: left;
1973 2021 }
1974 2022
1975 2023 .delete-notifications, .read-notifications{
1976 2024 width: 35px;
1977 2025 min-width: 35px; //fixes when only one button is displayed
1978 2026 }
1979 2027 }
1980 2028
1981 2029 .notification-body {
1982 2030 .markdown-block,
1983 2031 .rst-block {
1984 2032 padding: @padding 0;
1985 2033 }
1986 2034
1987 2035 .notification-subject {
1988 2036 padding: @textmargin 0;
1989 2037 border-bottom: @border-thickness solid @border-default-color;
1990 2038 }
1991 2039 }
1992 2040
1993 2041
1994 2042 .notifications_buttons{
1995 2043 float: right;
1996 2044 }
1997 2045
1998 2046 #notification-status{
1999 2047 display: inline;
2000 2048 }
2001 2049
2002 2050 // Repositories
2003 2051
2004 2052 #summary.fields{
2005 2053 display: table;
2006 2054
2007 2055 .field{
2008 2056 display: table-row;
2009 2057
2010 2058 .label-summary{
2011 2059 display: table-cell;
2012 2060 min-width: @label-summary-minwidth;
2013 2061 padding-top: @padding/2;
2014 2062 padding-bottom: @padding/2;
2015 2063 padding-right: @padding/2;
2016 2064 }
2017 2065
2018 2066 .input{
2019 2067 display: table-cell;
2020 2068 padding: @padding/2;
2021 2069
2022 2070 input{
2023 2071 min-width: 29em;
2024 2072 padding: @padding/4;
2025 2073 }
2026 2074 }
2027 2075 .statistics, .downloads{
2028 2076 .disabled{
2029 2077 color: @grey4;
2030 2078 }
2031 2079 }
2032 2080 }
2033 2081 }
2034 2082
2035 2083 #summary{
2036 2084 width: 70%;
2037 2085 }
2038 2086
2039 2087
2040 2088 // Journal
2041 2089 .journal.title {
2042 2090 h5 {
2043 2091 float: left;
2044 2092 margin: 0;
2045 2093 width: 70%;
2046 2094 }
2047 2095
2048 2096 ul {
2049 2097 float: right;
2050 2098 display: inline-block;
2051 2099 margin: 0;
2052 2100 width: 30%;
2053 2101 text-align: right;
2054 2102
2055 2103 li {
2056 2104 display: inline;
2057 2105 font-size: @journal-fontsize;
2058 2106 line-height: 1em;
2059 2107
2060 2108 list-style-type: none;
2061 2109 }
2062 2110 }
2063 2111 }
2064 2112
2065 2113 .filterexample {
2066 2114 position: absolute;
2067 2115 top: 95px;
2068 2116 left: @contentpadding;
2069 2117 color: @rcblue;
2070 2118 font-size: 11px;
2071 2119 font-family: @text-regular;
2072 2120 cursor: help;
2073 2121
2074 2122 &:hover {
2075 2123 color: @rcdarkblue;
2076 2124 }
2077 2125
2078 2126 @media (max-width:768px) {
2079 2127 position: relative;
2080 2128 top: auto;
2081 2129 left: auto;
2082 2130 display: block;
2083 2131 }
2084 2132 }
2085 2133
2086 2134
2087 2135 #journal{
2088 2136 margin-bottom: @space;
2089 2137
2090 2138 .journal_day{
2091 2139 margin-bottom: @textmargin/2;
2092 2140 padding-bottom: @textmargin/2;
2093 2141 font-size: @journal-fontsize;
2094 2142 border-bottom: @border-thickness solid @border-default-color;
2095 2143 }
2096 2144
2097 2145 .journal_container{
2098 2146 margin-bottom: @space;
2099 2147
2100 2148 .journal_user{
2101 2149 display: inline-block;
2102 2150 }
2103 2151 .journal_action_container{
2104 2152 display: block;
2105 2153 margin-top: @textmargin;
2106 2154
2107 2155 div{
2108 2156 display: inline;
2109 2157 }
2110 2158
2111 2159 div.journal_action_params{
2112 2160 display: block;
2113 2161 }
2114 2162
2115 2163 div.journal_repo:after{
2116 2164 content: "\A";
2117 2165 white-space: pre;
2118 2166 }
2119 2167
2120 2168 div.date{
2121 2169 display: block;
2122 2170 margin-bottom: @textmargin;
2123 2171 }
2124 2172 }
2125 2173 }
2126 2174 }
2127 2175
2128 2176 // Files
2129 2177 .edit-file-title {
2130 2178 font-size: 16px;
2131 2179
2132 2180 .title-heading {
2133 2181 padding: 2px;
2134 2182 }
2135 2183 }
2136 2184
2137 2185 .edit-file-fieldset {
2138 2186 margin: @sidebarpadding 0;
2139 2187
2140 2188 .fieldset {
2141 2189 .left-label {
2142 2190 width: 13%;
2143 2191 }
2144 2192 .right-content {
2145 2193 width: 87%;
2146 2194 max-width: 100%;
2147 2195 }
2148 2196 .filename-label {
2149 2197 margin-top: 13px;
2150 2198 }
2151 2199 .commit-message-label {
2152 2200 margin-top: 4px;
2153 2201 }
2154 2202 .file-upload-input {
2155 2203 input {
2156 2204 display: none;
2157 2205 }
2158 2206 margin-top: 10px;
2159 2207 }
2160 2208 .file-upload-label {
2161 2209 margin-top: 10px;
2162 2210 }
2163 2211 p {
2164 2212 margin-top: 5px;
2165 2213 }
2166 2214
2167 2215 }
2168 2216 .custom-path-link {
2169 2217 margin-left: 5px;
2170 2218 }
2171 2219 #commit {
2172 2220 resize: vertical;
2173 2221 }
2174 2222 }
2175 2223
2176 2224 .delete-file-preview {
2177 2225 max-height: 250px;
2178 2226 }
2179 2227
2180 2228 .new-file,
2181 2229 #filter_activate,
2182 2230 #filter_deactivate {
2183 2231 float: right;
2184 2232 margin: 0 0 0 10px;
2185 2233 }
2186 2234
2187 2235 .file-upload-transaction-wrapper {
2188 2236 margin-top: 57px;
2189 2237 clear: both;
2190 2238 }
2191 2239
2192 2240 .file-upload-transaction-wrapper .error {
2193 2241 color: @color5;
2194 2242 }
2195 2243
2196 2244 .file-upload-transaction {
2197 2245 min-height: 200px;
2198 2246 padding: 54px;
2199 2247 border: 1px solid @grey5;
2200 2248 text-align: center;
2201 2249 clear: both;
2202 2250 }
2203 2251
2204 2252 .file-upload-transaction i {
2205 2253 font-size: 48px
2206 2254 }
2207 2255
2208 2256 h3.files_location{
2209 2257 line-height: 2.4em;
2210 2258 }
2211 2259
2212 2260 .browser-nav {
2213 2261 width: 100%;
2214 2262 display: table;
2215 2263 margin-bottom: 20px;
2216 2264
2217 2265 .info_box {
2218 2266 float: left;
2219 2267 display: inline-table;
2220 2268 height: 2.5em;
2221 2269
2222 2270 .browser-cur-rev, .info_box_elem {
2223 2271 display: table-cell;
2224 2272 vertical-align: middle;
2225 2273 }
2226 2274
2227 2275 .drop-menu {
2228 2276 margin: 0 10px;
2229 2277 }
2230 2278
2231 2279 .info_box_elem {
2232 2280 border-top: @border-thickness solid @grey5;
2233 2281 border-bottom: @border-thickness solid @grey5;
2234 2282 box-shadow: @button-shadow;
2235 2283
2236 2284 #at_rev, a {
2237 2285 padding: 0.6em 0.4em;
2238 2286 margin: 0;
2239 2287 .box-shadow(none);
2240 2288 border: 0;
2241 2289 height: 12px;
2242 2290 color: @grey2;
2243 2291 }
2244 2292
2245 2293 input#at_rev {
2246 2294 max-width: 50px;
2247 2295 text-align: center;
2248 2296 }
2249 2297
2250 2298 &.previous {
2251 2299 border: @border-thickness solid @grey5;
2252 2300 border-top-left-radius: @border-radius;
2253 2301 border-bottom-left-radius: @border-radius;
2254 2302
2255 2303 &:hover {
2256 2304 border-color: @grey4;
2257 2305 }
2258 2306
2259 2307 .disabled {
2260 2308 color: @grey5;
2261 2309 cursor: not-allowed;
2262 2310 opacity: 0.5;
2263 2311 }
2264 2312 }
2265 2313
2266 2314 &.next {
2267 2315 border: @border-thickness solid @grey5;
2268 2316 border-top-right-radius: @border-radius;
2269 2317 border-bottom-right-radius: @border-radius;
2270 2318
2271 2319 &:hover {
2272 2320 border-color: @grey4;
2273 2321 }
2274 2322
2275 2323 .disabled {
2276 2324 color: @grey5;
2277 2325 cursor: not-allowed;
2278 2326 opacity: 0.5;
2279 2327 }
2280 2328 }
2281 2329 }
2282 2330
2283 2331 .browser-cur-rev {
2284 2332
2285 2333 span{
2286 2334 margin: 0;
2287 2335 color: @rcblue;
2288 2336 height: 12px;
2289 2337 display: inline-block;
2290 2338 padding: 0.7em 1em ;
2291 2339 border: @border-thickness solid @rcblue;
2292 2340 margin-right: @padding;
2293 2341 }
2294 2342 }
2295 2343
2296 2344 }
2297 2345
2298 2346 .select-index-number {
2299 2347 margin: 0 0 0 20px;
2300 2348 color: @grey3;
2301 2349 }
2302 2350
2303 2351 .search_activate {
2304 2352 display: table-cell;
2305 2353 vertical-align: middle;
2306 2354
2307 2355 input, label{
2308 2356 margin: 0;
2309 2357 padding: 0;
2310 2358 }
2311 2359
2312 2360 input{
2313 2361 margin-left: @textmargin;
2314 2362 }
2315 2363
2316 2364 }
2317 2365 }
2318 2366
2319 2367 .browser-cur-rev{
2320 2368 margin-bottom: @textmargin;
2321 2369 }
2322 2370
2323 2371 #node_filter_box_loading{
2324 2372 .info_text;
2325 2373 }
2326 2374
2327 2375 .browser-search {
2328 2376 margin: -25px 0px 5px 0px;
2329 2377 }
2330 2378
2331 2379 .files-quick-filter {
2332 2380 float: right;
2333 2381 width: 180px;
2334 2382 position: relative;
2335 2383 }
2336 2384
2337 2385 .files-filter-box {
2338 2386 display: flex;
2339 2387 padding: 0px;
2340 2388 border-radius: 3px;
2341 2389 margin-bottom: 0;
2342 2390
2343 2391 a {
2344 2392 border: none !important;
2345 2393 }
2346 2394
2347 2395 li {
2348 2396 list-style-type: none
2349 2397 }
2350 2398 }
2351 2399
2352 2400 .files-filter-box-path {
2353 2401 line-height: 33px;
2354 2402 padding: 0;
2355 2403 width: 20px;
2356 2404 position: absolute;
2357 2405 z-index: 11;
2358 2406 left: 5px;
2359 2407 }
2360 2408
2361 2409 .files-filter-box-input {
2362 2410 margin-right: 0;
2363 2411
2364 2412 input {
2365 2413 border: 1px solid @white;
2366 2414 padding-left: 25px;
2367 2415 width: 145px;
2368 2416
2369 2417 &:hover {
2370 2418 border-color: @grey6;
2371 2419 }
2372 2420
2373 2421 &:focus {
2374 2422 border-color: @grey5;
2375 2423 }
2376 2424 }
2377 2425 }
2378 2426
2379 2427 .browser-result{
2380 2428 td a{
2381 2429 margin-left: 0.5em;
2382 2430 display: inline-block;
2383 2431
2384 2432 em {
2385 2433 font-weight: @text-bold-weight;
2386 2434 font-family: @text-bold;
2387 2435 }
2388 2436 }
2389 2437 }
2390 2438
2391 2439 .browser-highlight{
2392 2440 background-color: @grey5-alpha;
2393 2441 }
2394 2442
2395 2443
2396 2444 .edit-file-fieldset #location,
2397 2445 .edit-file-fieldset #filename {
2398 2446 display: flex;
2399 2447 width: -moz-available; /* WebKit-based browsers will ignore this. */
2400 2448 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2401 2449 width: fill-available;
2402 2450 border: 0;
2403 2451 }
2404 2452
2405 2453 .path-items {
2406 2454 display: flex;
2407 2455 padding: 0;
2408 2456 border: 1px solid #eeeeee;
2409 2457 width: 100%;
2410 2458 float: left;
2411 2459
2412 2460 .breadcrumb-path {
2413 2461 line-height: 30px;
2414 2462 padding: 0 4px;
2415 2463 white-space: nowrap;
2416 2464 }
2417 2465
2418 2466 .location-path {
2419 2467 width: -moz-available; /* WebKit-based browsers will ignore this. */
2420 2468 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2421 2469 width: fill-available;
2422 2470
2423 2471 .file-name-input {
2424 2472 padding: 0.5em 0;
2425 2473 }
2426 2474
2427 2475 }
2428 2476
2429 2477 ul {
2430 2478 display: flex;
2431 2479 margin: 0;
2432 2480 padding: 0;
2433 2481 width: 100%;
2434 2482 }
2435 2483
2436 2484 li {
2437 2485 list-style-type: none;
2438 2486 }
2439 2487
2440 2488 }
2441 2489
2442 2490 .editor-items {
2443 2491 height: 40px;
2444 2492 margin: 10px 0 -17px 10px;
2445 2493
2446 2494 .editor-action {
2447 2495 cursor: pointer;
2448 2496 }
2449 2497
2450 2498 .editor-action.active {
2451 2499 border-bottom: 2px solid #5C5C5C;
2452 2500 }
2453 2501
2454 2502 li {
2455 2503 list-style-type: none;
2456 2504 }
2457 2505 }
2458 2506
2459 2507 .edit-file-fieldset .message textarea {
2460 2508 border: 1px solid #eeeeee;
2461 2509 }
2462 2510
2463 2511 #files_data .codeblock {
2464 2512 background-color: #F5F5F5;
2465 2513 }
2466 2514
2467 2515 #editor_preview {
2468 2516 background: white;
2469 2517 }
2470 2518
2471 2519 .show-editor {
2472 2520 padding: 10px;
2473 2521 background-color: white;
2474 2522
2475 2523 }
2476 2524
2477 2525 .show-preview {
2478 2526 padding: 10px;
2479 2527 background-color: white;
2480 2528 border-left: 1px solid #eeeeee;
2481 2529 }
2482 2530 // quick filter
2483 2531 .grid-quick-filter {
2484 2532 float: right;
2485 2533 position: relative;
2486 2534 }
2487 2535
2488 2536 .grid-filter-box {
2489 2537 display: flex;
2490 2538 padding: 0px;
2491 2539 border-radius: 3px;
2492 2540 margin-bottom: 0;
2493 2541
2494 2542 a {
2495 2543 border: none !important;
2496 2544 }
2497 2545
2498 2546 li {
2499 2547 list-style-type: none
2500 2548 }
2501 2549 }
2502 2550
2503 2551 .grid-filter-box-icon {
2504 2552 line-height: 33px;
2505 2553 padding: 0;
2506 2554 width: 20px;
2507 2555 position: absolute;
2508 2556 z-index: 11;
2509 2557 left: 5px;
2510 2558 }
2511 2559
2512 2560 .grid-filter-box-input {
2513 2561 margin-right: 0;
2514 2562
2515 2563 input {
2516 2564 border: 1px solid @white;
2517 2565 padding-left: 25px;
2518 2566 width: 145px;
2519 2567
2520 2568 &:hover {
2521 2569 border-color: @grey6;
2522 2570 }
2523 2571
2524 2572 &:focus {
2525 2573 border-color: @grey5;
2526 2574 }
2527 2575 }
2528 2576 }
2529 2577
2530 2578
2531 2579
2532 2580 // Search
2533 2581
2534 2582 .search-form{
2535 2583 #q {
2536 2584 width: @search-form-width;
2537 2585 }
2538 2586 .fields{
2539 2587 margin: 0 0 @space;
2540 2588 }
2541 2589
2542 2590 label{
2543 2591 display: inline-block;
2544 2592 margin-right: @textmargin;
2545 2593 padding-top: 0.25em;
2546 2594 }
2547 2595
2548 2596
2549 2597 .results{
2550 2598 clear: both;
2551 2599 margin: 0 0 @padding;
2552 2600 }
2553 2601
2554 2602 .search-tags {
2555 2603 padding: 5px 0;
2556 2604 }
2557 2605 }
2558 2606
2559 2607 div.search-feedback-items {
2560 2608 display: inline-block;
2561 2609 }
2562 2610
2563 2611 div.search-code-body {
2564 2612 background-color: #ffffff; padding: 5px 0 5px 10px;
2565 2613 pre {
2566 2614 .match { background-color: #faffa6;}
2567 2615 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2568 2616 }
2569 2617 }
2570 2618
2571 2619 .expand_commit.search {
2572 2620 .show_more.open {
2573 2621 height: auto;
2574 2622 max-height: none;
2575 2623 }
2576 2624 }
2577 2625
2578 2626 .search-results {
2579 2627
2580 2628 h2 {
2581 2629 margin-bottom: 0;
2582 2630 }
2583 2631 .codeblock {
2584 2632 border: none;
2585 2633 background: transparent;
2586 2634 }
2587 2635
2588 2636 .codeblock-header {
2589 2637 border: none;
2590 2638 background: transparent;
2591 2639 }
2592 2640
2593 2641 .code-body {
2594 2642 border: @border-thickness solid @grey6;
2595 2643 .border-radius(@border-radius);
2596 2644 }
2597 2645
2598 2646 .td-commit {
2599 2647 &:extend(pre);
2600 2648 border-bottom: @border-thickness solid @border-default-color;
2601 2649 }
2602 2650
2603 2651 .message {
2604 2652 height: auto;
2605 2653 max-width: 350px;
2606 2654 white-space: normal;
2607 2655 text-overflow: initial;
2608 2656 overflow: visible;
2609 2657
2610 2658 .match { background-color: #faffa6;}
2611 2659 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2612 2660 }
2613 2661
2614 2662 .path {
2615 2663 border-bottom: none !important;
2616 2664 border-left: 1px solid @grey6 !important;
2617 2665 border-right: 1px solid @grey6 !important;
2618 2666 }
2619 2667 }
2620 2668
2621 2669 table.rctable td.td-search-results div {
2622 2670 max-width: 100%;
2623 2671 }
2624 2672
2625 2673 #tip-box, .tip-box{
2626 2674 padding: @menupadding/2;
2627 2675 display: block;
2628 2676 border: @border-thickness solid @border-highlight-color;
2629 2677 .border-radius(@border-radius);
2630 2678 background-color: white;
2631 2679 z-index: 99;
2632 2680 white-space: pre-wrap;
2633 2681 }
2634 2682
2635 2683 #linktt {
2636 2684 width: 79px;
2637 2685 }
2638 2686
2639 2687 #help_kb .modal-content{
2640 2688 max-width: 750px;
2641 2689 margin: 10% auto;
2642 2690
2643 2691 table{
2644 2692 td,th{
2645 2693 border-bottom: none;
2646 2694 line-height: 2.5em;
2647 2695 }
2648 2696 th{
2649 2697 padding-bottom: @textmargin/2;
2650 2698 }
2651 2699 td.keys{
2652 2700 text-align: center;
2653 2701 }
2654 2702 }
2655 2703
2656 2704 .block-left{
2657 2705 width: 45%;
2658 2706 margin-right: 5%;
2659 2707 }
2660 2708 .modal-footer{
2661 2709 clear: both;
2662 2710 }
2663 2711 .key.tag{
2664 2712 padding: 0.5em;
2665 2713 background-color: @rcblue;
2666 2714 color: white;
2667 2715 border-color: @rcblue;
2668 2716 .box-shadow(none);
2669 2717 }
2670 2718 }
2671 2719
2672 2720
2673 2721
2674 2722 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2675 2723
2676 2724 @import 'statistics-graph';
2677 2725 @import 'tables';
2678 2726 @import 'forms';
2679 2727 @import 'diff';
2680 2728 @import 'summary';
2681 2729 @import 'navigation';
2682 2730
2683 2731 //--- SHOW/HIDE SECTIONS --//
2684 2732
2685 2733 .btn-collapse {
2686 2734 float: right;
2687 2735 text-align: right;
2688 2736 font-family: @text-light;
2689 2737 font-size: @basefontsize;
2690 2738 cursor: pointer;
2691 2739 border: none;
2692 2740 color: @rcblue;
2693 2741 }
2694 2742
2695 2743 table.rctable,
2696 2744 table.dataTable {
2697 2745 .btn-collapse {
2698 2746 float: right;
2699 2747 text-align: right;
2700 2748 }
2701 2749 }
2702 2750
2703 2751 table.rctable {
2704 2752 &.permissions {
2705 2753
2706 2754 th.td-owner {
2707 2755 padding: 0;
2708 2756 }
2709 2757
2710 2758 th {
2711 2759 font-weight: normal;
2712 2760 padding: 0 5px;
2713 2761 }
2714 2762
2715 2763 }
2716 2764 }
2717 2765
2718 2766
2719 2767 // TODO: johbo: Fix for IE10, this avoids that we see a border
2720 2768 // and padding around checkboxes and radio boxes. Move to the right place,
2721 2769 // or better: Remove this once we did the form refactoring.
2722 2770 input[type=checkbox],
2723 2771 input[type=radio] {
2724 2772 padding: 0;
2725 2773 border: none;
2726 2774 }
2727 2775
2728 2776 .toggle-ajax-spinner{
2729 2777 height: 16px;
2730 2778 width: 16px;
2731 2779 }
2732 2780
2733 2781
2734 2782 .markup-form .clearfix {
2735 2783 .border-radius(@border-radius);
2736 2784 margin: 0px;
2737 2785 }
2738 2786
2739 2787 .markup-form-area {
2740 2788 padding: 8px 12px;
2741 2789 border: 1px solid @grey4;
2742 2790 .border-radius(@border-radius);
2743 2791 }
2744 2792
2745 2793 .markup-form-area-header .nav-links {
2746 2794 display: flex;
2747 2795 flex-flow: row wrap;
2748 2796 -webkit-flex-flow: row wrap;
2749 2797 width: 100%;
2750 2798 }
2751 2799
2752 2800 .markup-form-area-footer {
2753 2801 display: flex;
2754 2802 }
2755 2803
2756 2804 .markup-form-area-footer .toolbar {
2757 2805
2758 2806 }
2759 2807
2760 2808 // markup Form
2761 2809 div.markup-form {
2762 2810 margin-top: 20px;
2763 2811 }
2764 2812
2765 2813 .markup-form strong {
2766 2814 display: block;
2767 2815 margin-bottom: 15px;
2768 2816 }
2769 2817
2770 2818 .markup-form textarea {
2771 2819 width: 100%;
2772 2820 height: 100px;
2773 2821 font-family: @text-monospace;
2774 2822 }
2775 2823
2776 2824 form.markup-form {
2777 2825 margin-top: 10px;
2778 2826 margin-left: 10px;
2779 2827 }
2780 2828
2781 2829 .markup-form .comment-block-ta,
2782 2830 .markup-form .preview-box {
2783 2831 .border-radius(@border-radius);
2784 2832 .box-sizing(border-box);
2785 2833 background-color: white;
2786 2834 }
2787 2835
2788 2836 .markup-form .preview-box.unloaded {
2789 2837 height: 50px;
2790 2838 text-align: center;
2791 2839 padding: 20px;
2792 2840 background-color: white;
2793 2841 }
2794 2842
2795 2843
2796 2844 .dropzone-wrapper {
2797 2845 border: 1px solid @grey5;
2798 2846 padding: 20px;
2799 2847 }
2800 2848
2801 2849 .dropzone,
2802 2850 .dropzone-pure {
2803 2851 border: 2px dashed @grey5;
2804 2852 border-radius: 5px;
2805 2853 background: white;
2806 2854 min-height: 200px;
2807 2855 padding: 54px;
2808 2856
2809 2857 .dz-message {
2810 2858 font-weight: 700;
2811 2859 text-align: center;
2812 2860 margin: 2em 0;
2813 2861 }
2814 2862
2815 2863 }
2816 2864
2817 2865 .dz-preview {
2818 2866 margin: 10px 0 !important;
2819 2867 position: relative;
2820 2868 vertical-align: top;
2821 2869 padding: 10px;
2822 2870 border-bottom: 1px solid @grey5;
2823 2871 }
2824 2872
2825 2873 .dz-filename {
2826 2874 font-weight: 700;
2827 2875 float: left;
2828 2876 }
2829 2877
2830 2878 .dz-sending {
2831 2879 float: right;
2832 2880 }
2833 2881
2834 2882 .dz-response {
2835 2883 clear: both
2836 2884 }
2837 2885
2838 2886 .dz-filename-size {
2839 2887 float: right
2840 2888 }
2841 2889
2842 2890 .dz-error-message {
2843 2891 color: @alert2;
2844 2892 padding-top: 10px;
2845 2893 clear: both;
2846 2894 }
2847 2895
2848 2896
2849 2897 .user-hovercard {
2850 2898 padding: 5px;
2851 2899 }
2852 2900
2853 2901 .user-hovercard-icon {
2854 2902 display: inline;
2855 2903 padding: 0;
2856 2904 box-sizing: content-box;
2857 2905 border-radius: 50%;
2858 2906 float: left;
2859 2907 }
2860 2908
2861 2909 .user-hovercard-name {
2862 2910 float: right;
2863 2911 vertical-align: top;
2864 2912 padding-left: 10px;
2865 2913 min-width: 150px;
2866 2914 }
2867 2915
2868 2916 .user-hovercard-bio {
2869 2917 clear: both;
2870 2918 padding-top: 10px;
2871 2919 }
2872 2920
2873 2921 .user-hovercard-header {
2874 2922 clear: both;
2875 2923 min-height: 10px;
2876 2924 }
2877 2925
2878 2926 .user-hovercard-footer {
2879 2927 clear: both;
2880 2928 min-height: 10px;
2881 2929 }
2882 2930
2883 2931 .user-group-hovercard {
2884 2932 padding: 5px;
2885 2933 }
2886 2934
2887 2935 .user-group-hovercard-icon {
2888 2936 display: inline;
2889 2937 padding: 0;
2890 2938 box-sizing: content-box;
2891 2939 border-radius: 50%;
2892 2940 float: left;
2893 2941 }
2894 2942
2895 2943 .user-group-hovercard-name {
2896 2944 float: left;
2897 2945 vertical-align: top;
2898 2946 padding-left: 10px;
2899 2947 min-width: 150px;
2900 2948 }
2901 2949
2902 2950 .user-group-hovercard-icon i {
2903 2951 border: 1px solid @grey4;
2904 2952 border-radius: 4px;
2905 2953 }
2906 2954
2907 2955 .user-group-hovercard-bio {
2908 2956 clear: both;
2909 2957 padding-top: 10px;
2910 2958 line-height: 1.0em;
2911 2959 }
2912 2960
2913 2961 .user-group-hovercard-header {
2914 2962 clear: both;
2915 2963 min-height: 10px;
2916 2964 }
2917 2965
2918 2966 .user-group-hovercard-footer {
2919 2967 clear: both;
2920 2968 min-height: 10px;
2921 2969 }
2922 2970
2923 2971 .pr-hovercard-header {
2924 2972 clear: both;
2925 2973 display: block;
2926 2974 line-height: 20px;
2927 2975 }
2928 2976
2929 2977 .pr-hovercard-user {
2930 2978 display: flex;
2931 2979 align-items: center;
2932 2980 padding-left: 5px;
2933 2981 }
2934 2982
2935 2983 .pr-hovercard-title {
2936 2984 padding-top: 5px;
2937 2985 } No newline at end of file
@@ -1,608 +1,623 b''
1 1 // # Copyright (C) 2010-2019 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(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).find('td.td-description .message').data('messageRaw');
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 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
84 84 }
85 85 else {
86 86 // use reference name
87 87 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 88 }
89 89
90 90 return [title, desc]
91 91 };
92 92
93 93
94 94
95 95 ReviewersController = function () {
96 96 var self = this;
97 97 this.$reviewRulesContainer = $('#review_rules');
98 98 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
99 99 this.forbidReviewUsers = undefined;
100 100 this.$reviewMembers = $('#review_members');
101 101 this.currentRequest = null;
102 102
103 103 this.defaultForbidReviewUsers = function() {
104 104 return [
105 105 {'username': 'default',
106 106 'user_id': templateContext.default_user.user_id}
107 107 ];
108 108 };
109 109
110 110 this.hideReviewRules = function() {
111 111 self.$reviewRulesContainer.hide();
112 112 };
113 113
114 114 this.showReviewRules = function() {
115 115 self.$reviewRulesContainer.show();
116 116 };
117 117
118 118 this.addRule = function(ruleText) {
119 119 self.showReviewRules();
120 120 return '<div>- {0}</div>'.format(ruleText)
121 121 };
122 122
123 123 this.loadReviewRules = function(data) {
124 124 // reset forbidden Users
125 125 this.forbidReviewUsers = self.defaultForbidReviewUsers();
126 126
127 127 // reset state of review rules
128 128 self.$rulesList.html('');
129 129
130 130 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
131 131 // default rule, case for older repo that don't have any rules stored
132 132 self.$rulesList.append(
133 133 self.addRule(
134 134 _gettext('All reviewers must vote.'))
135 135 );
136 136 return self.forbidReviewUsers
137 137 }
138 138
139 139 if (data.rules.voting !== undefined) {
140 140 if (data.rules.voting < 0) {
141 141 self.$rulesList.append(
142 142 self.addRule(
143 143 _gettext('All individual reviewers must vote.'))
144 144 )
145 145 } else if (data.rules.voting === 1) {
146 146 self.$rulesList.append(
147 147 self.addRule(
148 148 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
149 149 )
150 150
151 151 } else {
152 152 self.$rulesList.append(
153 153 self.addRule(
154 154 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
155 155 )
156 156 }
157 157 }
158 158
159 159 if (data.rules.voting_groups !== undefined) {
160 160 $.each(data.rules.voting_groups, function(index, rule_data) {
161 161 self.$rulesList.append(
162 162 self.addRule(rule_data.text)
163 163 )
164 164 });
165 165 }
166 166
167 167 if (data.rules.use_code_authors_for_review) {
168 168 self.$rulesList.append(
169 169 self.addRule(
170 170 _gettext('Reviewers picked from source code changes.'))
171 171 )
172 172 }
173 173 if (data.rules.forbid_adding_reviewers) {
174 174 $('#add_reviewer_input').remove();
175 175 self.$rulesList.append(
176 176 self.addRule(
177 177 _gettext('Adding new reviewers is forbidden.'))
178 178 )
179 179 }
180 180 if (data.rules.forbid_author_to_review) {
181 181 self.forbidReviewUsers.push(data.rules_data.pr_author);
182 182 self.$rulesList.append(
183 183 self.addRule(
184 184 _gettext('Author is not allowed to be a reviewer.'))
185 185 )
186 186 }
187 187 if (data.rules.forbid_commit_author_to_review) {
188 188
189 189 if (data.rules_data.forbidden_users) {
190 190 $.each(data.rules_data.forbidden_users, function(index, member_data) {
191 191 self.forbidReviewUsers.push(member_data)
192 192 });
193 193
194 194 }
195 195
196 196 self.$rulesList.append(
197 197 self.addRule(
198 198 _gettext('Commit Authors are not allowed to be a reviewer.'))
199 199 )
200 200 }
201 201
202 202 return self.forbidReviewUsers
203 203 };
204 204
205 205 this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) {
206 206
207 207 if (self.currentRequest) {
208 208 // make sure we cleanup old running requests before triggering this
209 209 // again
210 210 self.currentRequest.abort();
211 211 }
212 212
213 213 $('.calculate-reviewers').show();
214 214 // reset reviewer members
215 215 self.$reviewMembers.empty();
216 216
217 217 prButtonLock(true, null, 'reviewers');
218 218 $('#user').hide(); // hide user autocomplete before load
219 219
220 220 if (sourceRef.length !== 3 || targetRef.length !== 3) {
221 221 // don't load defaults in case we're missing some refs...
222 222 $('.calculate-reviewers').hide();
223 223 return
224 224 }
225 225
226 226 var url = pyroutes.url('repo_default_reviewers_data',
227 227 {
228 228 'repo_name': templateContext.repo_name,
229 229 'source_repo': sourceRepo,
230 230 'source_ref': sourceRef[2],
231 231 'target_repo': targetRepo,
232 232 'target_ref': targetRef[2]
233 233 });
234 234
235 235 self.currentRequest = $.get(url)
236 236 .done(function(data) {
237 237 self.currentRequest = null;
238 238
239 239 // review rules
240 240 self.loadReviewRules(data);
241 241
242 242 for (var i = 0; i < data.reviewers.length; i++) {
243 243 var reviewer = data.reviewers[i];
244 244 self.addReviewMember(
245 245 reviewer, reviewer.reasons, reviewer.mandatory);
246 246 }
247 247 $('.calculate-reviewers').hide();
248 248 prButtonLock(false, null, 'reviewers');
249 249 $('#user').show(); // show user autocomplete after load
250 250 });
251 251 };
252 252
253 253 // check those, refactor
254 254 this.removeReviewMember = function(reviewer_id, mark_delete) {
255 255 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
256 256
257 257 if(typeof(mark_delete) === undefined){
258 258 mark_delete = false;
259 259 }
260 260
261 261 if(mark_delete === true){
262 262 if (reviewer){
263 263 // now delete the input
264 264 $('#reviewer_{0} input'.format(reviewer_id)).remove();
265 265 // mark as to-delete
266 266 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
267 267 obj.addClass('to-delete');
268 268 obj.css({"text-decoration":"line-through", "opacity": 0.5});
269 269 }
270 270 }
271 271 else{
272 272 $('#reviewer_{0}'.format(reviewer_id)).remove();
273 273 }
274 274 };
275 275 this.reviewMemberEntry = function() {
276 276
277 277 };
278 278 this.addReviewMember = function(reviewer_obj, reasons, mandatory) {
279 279 var members = self.$reviewMembers.get(0);
280 280 var id = reviewer_obj.user_id;
281 281 var username = reviewer_obj.username;
282 282
283 283 var reasons = reasons || [];
284 284 var mandatory = mandatory || false;
285 285
286 286 // register IDS to check if we don't have this ID already in
287 287 var currentIds = [];
288 288 var _els = self.$reviewMembers.find('li').toArray();
289 289 for (el in _els){
290 290 currentIds.push(_els[el].id)
291 291 }
292 292
293 293 var userAllowedReview = function(userId) {
294 294 var allowed = true;
295 295 $.each(self.forbidReviewUsers, function(index, member_data) {
296 296 if (parseInt(userId) === member_data['user_id']) {
297 297 allowed = false;
298 298 return false // breaks the loop
299 299 }
300 300 });
301 301 return allowed
302 302 };
303 303
304 304 var userAllowed = userAllowedReview(id);
305 305 if (!userAllowed){
306 306 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
307 307 } else {
308 308 // only add if it's not there
309 309 var alreadyReviewer = currentIds.indexOf('reviewer_'+id) != -1;
310 310
311 311 if (alreadyReviewer) {
312 312 alert(_gettext('User `{0}` already in reviewers').format(username));
313 313 } else {
314 314 members.innerHTML += renderTemplate('reviewMemberEntry', {
315 315 'member': reviewer_obj,
316 316 'mandatory': mandatory,
317 317 'allowed_to_update': true,
318 318 'review_status': 'not_reviewed',
319 319 'review_status_label': _gettext('Not Reviewed'),
320 320 'reasons': reasons,
321 321 'create': true
322 322 });
323 323 tooltipActivate();
324 324 }
325 325 }
326 326
327 327 };
328 328
329 329 this.updateReviewers = function(repo_name, pull_request_id){
330 330 var postData = $('#reviewers input').serialize();
331 331 _updatePullRequest(repo_name, pull_request_id, postData);
332 332 };
333 333
334 334 };
335 335
336 336
337 337 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
338 338 var url = pyroutes.url(
339 339 'pullrequest_update',
340 340 {"repo_name": repo_name, "pull_request_id": pull_request_id});
341 341 if (typeof postData === 'string' ) {
342 342 postData += '&csrf_token=' + CSRF_TOKEN;
343 343 } else {
344 344 postData.csrf_token = CSRF_TOKEN;
345 345 }
346 346
347 347 var success = function(o) {
348 348 var redirectUrl = o['redirect_url'];
349 349 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
350 350 window.location = redirectUrl;
351 351 } else {
352 352 window.location.reload();
353 353 }
354 354 };
355 355
356 356 ajaxPOST(url, postData, success);
357 357 };
358 358
359 359 /**
360 360 * PULL REQUEST update commits
361 361 */
362 362 var updateCommits = function(repo_name, pull_request_id, force) {
363 363 var postData = {
364 364 'update_commits': true
365 365 };
366 366 if (force !== undefined && force === true) {
367 367 postData['force_refresh'] = true
368 368 }
369 369 _updatePullRequest(repo_name, pull_request_id, postData);
370 370 };
371 371
372 372
373 373 /**
374 374 * PULL REQUEST edit info
375 375 */
376 376 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
377 377 var url = pyroutes.url(
378 378 'pullrequest_update',
379 379 {"repo_name": repo_name, "pull_request_id": pull_request_id});
380 380
381 381 var postData = {
382 382 'title': title,
383 383 'description': description,
384 384 'description_renderer': renderer,
385 385 'edit_pull_request': true,
386 386 'csrf_token': CSRF_TOKEN
387 387 };
388 388 var success = function(o) {
389 389 window.location.reload();
390 390 };
391 391 ajaxPOST(url, postData, success);
392 392 };
393 393
394 394
395 395 /**
396 396 * Reviewer autocomplete
397 397 */
398 398 var ReviewerAutoComplete = function(inputId) {
399 399 $(inputId).autocomplete({
400 400 serviceUrl: pyroutes.url('user_autocomplete_data'),
401 401 minChars:2,
402 402 maxHeight:400,
403 403 deferRequestBy: 300, //miliseconds
404 404 showNoSuggestionNotice: true,
405 405 tabDisabled: true,
406 406 autoSelectFirst: true,
407 407 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
408 408 formatResult: autocompleteFormatResult,
409 409 lookupFilter: autocompleteFilterResult,
410 410 onSelect: function(element, data) {
411 411 var mandatory = false;
412 412 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
413 413
414 414 // add whole user groups
415 415 if (data.value_type == 'user_group') {
416 416 reasons.push(_gettext('member of "{0}"').format(data.value_display));
417 417
418 418 $.each(data.members, function(index, member_data) {
419 419 var reviewer = member_data;
420 420 reviewer['user_id'] = member_data['id'];
421 421 reviewer['gravatar_link'] = member_data['icon_link'];
422 422 reviewer['user_link'] = member_data['profile_link'];
423 423 reviewer['rules'] = [];
424 424 reviewersController.addReviewMember(reviewer, reasons, mandatory);
425 425 })
426 426 }
427 427 // add single user
428 428 else {
429 429 var reviewer = data;
430 430 reviewer['user_id'] = data['id'];
431 431 reviewer['gravatar_link'] = data['icon_link'];
432 432 reviewer['user_link'] = data['profile_link'];
433 433 reviewer['rules'] = [];
434 434 reviewersController.addReviewMember(reviewer, reasons, mandatory);
435 435 }
436 436
437 437 $(inputId).val('');
438 438 }
439 439 });
440 440 };
441 441
442 442
443 443 VersionController = function () {
444 444 var self = this;
445 445 this.$verSource = $('input[name=ver_source]');
446 446 this.$verTarget = $('input[name=ver_target]');
447 447 this.$showVersionDiff = $('#show-version-diff');
448 448
449 449 this.adjustRadioSelectors = function (curNode) {
450 450 var getVal = function (item) {
451 451 if (item == 'latest') {
452 452 return Number.MAX_SAFE_INTEGER
453 453 }
454 454 else {
455 455 return parseInt(item)
456 456 }
457 457 };
458 458
459 459 var curVal = getVal($(curNode).val());
460 460 var cleared = false;
461 461
462 462 $.each(self.$verSource, function (index, value) {
463 463 var elVal = getVal($(value).val());
464 464
465 465 if (elVal > curVal) {
466 466 if ($(value).is(':checked')) {
467 467 cleared = true;
468 468 }
469 469 $(value).attr('disabled', 'disabled');
470 470 $(value).removeAttr('checked');
471 471 $(value).css({'opacity': 0.1});
472 472 }
473 473 else {
474 474 $(value).css({'opacity': 1});
475 475 $(value).removeAttr('disabled');
476 476 }
477 477 });
478 478
479 479 if (cleared) {
480 480 // if we unchecked an active, set the next one to same loc.
481 481 $(this.$verSource).filter('[value={0}]'.format(
482 482 curVal)).attr('checked', 'checked');
483 483 }
484 484
485 485 self.setLockAction(false,
486 486 $(curNode).data('verPos'),
487 487 $(this.$verSource).filter(':checked').data('verPos')
488 488 );
489 489 };
490 490
491 491
492 492 this.attachVersionListener = function () {
493 493 self.$verTarget.change(function (e) {
494 494 self.adjustRadioSelectors(this)
495 495 });
496 496 self.$verSource.change(function (e) {
497 497 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
498 498 });
499 499 };
500 500
501 501 this.init = function () {
502 502
503 503 var curNode = self.$verTarget.filter(':checked');
504 504 self.adjustRadioSelectors(curNode);
505 505 self.setLockAction(true);
506 506 self.attachVersionListener();
507 507
508 508 };
509 509
510 510 this.setLockAction = function (state, selectedVersion, otherVersion) {
511 511 var $showVersionDiff = this.$showVersionDiff;
512 512
513 513 if (state) {
514 514 $showVersionDiff.attr('disabled', 'disabled');
515 515 $showVersionDiff.addClass('disabled');
516 516 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
517 517 }
518 518 else {
519 519 $showVersionDiff.removeAttr('disabled');
520 520 $showVersionDiff.removeClass('disabled');
521 521
522 522 if (selectedVersion == otherVersion) {
523 523 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
524 524 } else {
525 525 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
526 526 }
527 527 }
528 528
529 529 };
530 530
531 531 this.showVersionDiff = function () {
532 532 var target = self.$verTarget.filter(':checked');
533 533 var source = self.$verSource.filter(':checked');
534 534
535 535 if (target.val() && source.val()) {
536 536 var params = {
537 537 'pull_request_id': templateContext.pull_request_data.pull_request_id,
538 538 'repo_name': templateContext.repo_name,
539 539 'version': target.val(),
540 540 'from_version': source.val()
541 541 };
542 542 window.location = pyroutes.url('pullrequest_show', params)
543 543 }
544 544
545 545 return false;
546 546 };
547 547
548 548 this.toggleVersionView = function (elem) {
549 549
550 550 if (this.$showVersionDiff.is(':visible')) {
551 551 $('.version-pr').hide();
552 552 this.$showVersionDiff.hide();
553 553 $(elem).html($(elem).data('toggleOn'))
554 554 } else {
555 555 $('.version-pr').show();
556 556 this.$showVersionDiff.show();
557 557 $(elem).html($(elem).data('toggleOff'))
558 558 }
559 559
560 560 return false
561 };
562
563 this.toggleElement = function (elem, target) {
564 var $elem = $(elem);
565 var $target = $(target);
566
567 if ($target.is(':visible')) {
568 $target.hide();
569 $elem.html($elem.data('toggleOn'))
570 } else {
571 $target.show();
572 $elem.html($elem.data('toggleOff'))
573 }
574
575 return false
561 576 }
562 577
563 578 };
564 579
565 580
566 581 UpdatePrController = function () {
567 582 var self = this;
568 583 this.$updateCommits = $('#update_commits');
569 584 this.$updateCommitsSwitcher = $('#update_commits_switcher');
570 585
571 586 this.lockUpdateButton = function (label) {
572 587 self.$updateCommits.attr('disabled', 'disabled');
573 588 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
574 589
575 590 self.$updateCommits.addClass('disabled');
576 591 self.$updateCommitsSwitcher.addClass('disabled');
577 592
578 593 self.$updateCommits.removeClass('btn-primary');
579 594 self.$updateCommitsSwitcher.removeClass('btn-primary');
580 595
581 596 self.$updateCommits.text(_gettext(label));
582 597 };
583 598
584 599 this.isUpdateLocked = function () {
585 600 return self.$updateCommits.attr('disabled') !== undefined;
586 601 };
587 602
588 603 this.updateCommits = function (curNode) {
589 604 if (self.isUpdateLocked()) {
590 605 return
591 606 }
592 607 self.lockUpdateButton(_gettext('Updating...'));
593 608 updateCommits(
594 609 templateContext.repo_name,
595 610 templateContext.pull_request_data.pull_request_id);
596 611 };
597 612
598 613 this.forceUpdateCommits = function () {
599 614 if (self.isUpdateLocked()) {
600 615 return
601 616 }
602 617 self.lockUpdateButton(_gettext('Force updating...'));
603 618 var force = true;
604 619 updateCommits(
605 620 templateContext.repo_name,
606 621 templateContext.pull_request_data.pull_request_id, force);
607 622 };
608 623 }; No newline at end of file
This diff has been collapsed as it changes many lines, (877 lines changed) Show them Hide them
@@ -1,819 +1,842 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 12 <%def name="breadcrumbs_links()">
13 <%
14 pr_title = c.pull_request.title
15 if c.pull_request.is_closed():
16 pr_title = '[{}] {}'.format(_('Closed'), pr_title)
17 %>
18 13
19 14 <div id="pr-title">
20 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${pr_title}">
15 % if c.pull_request.is_closed():
16 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
17 % endif
18 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
21 19 </div>
22 20 <div id="pr-title-edit" class="input" style="display: none;">
23 21 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
24 22 </div>
25 23 </%def>
26 24
27 25 <%def name="menu_bar_nav()">
28 26 ${self.menu_items(active='repositories')}
29 27 </%def>
30 28
31 29 <%def name="menu_bar_subnav()">
32 30 ${self.repo_menu(active='showpullrequest')}
33 31 </%def>
34 32
35 33 <%def name="main()">
36 34
37 35 <script type="text/javascript">
38 36 // TODO: marcink switch this to pyroutes
39 37 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
40 38 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
41 39 </script>
40
42 41 <div class="box">
43 42
44 43 ${self.breadcrumbs()}
45 44
46 45 <div class="box pr-summary">
47 46
48 <div class="summary-details block-left">
49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 <div class="pr-details-title">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 %if c.allowed_to_update:
53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 % if c.allowed_to_delete:
55 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 ${h.end_form()}
59 % else:
60 ${_('Delete')}
61 % endif
62 </div>
63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 %endif
47 <div class="summary-details block-left">
48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <div class="pr-details-title">
50 <div class="pull-left">
51 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
52 ${_('Created on')}
53 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
54 <span class="pr-details-title-author-pref">${_('by')}</span>
55 </div>
56
57 <div class="pull-left">
58 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
66 59 </div>
67 60
68 <div id="summary" class="fields pr-details-content">
69 <div class="field">
70 <div class="label-summary">
71 <label>${_('Source')}:</label>
72 </div>
73 <div class="input">
74 <div class="pr-origininfo">
75 ## branch link is only valid if it is a branch
76 <span class="tag">
77 %if c.pull_request.source_ref_parts.type == 'branch':
78 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 %else:
80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 %endif
82 </span>
83 <span class="clone-url">
84 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 </span>
86 <br/>
87 % if c.ancestor_commit:
88 ${_('Common ancestor')}:
89 <code><a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 % endif
91 </div>
92 %if h.is_hg(c.pull_request.source_repo):
93 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
94 %elif h.is_git(c.pull_request.source_repo):
95 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
61 %if c.allowed_to_update:
62 <div id="delete_pullrequest" class="pull-right action_button ${('' if c.allowed_to_delete else 'disabled' )}" >
63 % if c.allowed_to_delete:
64 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
65 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
66 class_="btn btn-link btn-danger no-margin",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
67 ${h.end_form()}
68 % else:
69 ${_('Delete')}
70 % endif
71 </div>
72 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
73 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;">${_('Cancel')}</div>
74 <div id="edit_pull_request" class="pull-right action_button pr-save" style="display: none;">${_('Save Changes')}</div>
75 %endif
76 </div>
77
78 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
79 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
80 </div>
81
82 <div id="pr-desc-edit" class="input textarea" style="display: none;">
83 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
84 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
85 </div>
86
87 <div id="summary" class="fields pr-details-content">
88
89 ## review
90 <div class="field">
91 <div class="label-pr-detail">
92 <label>${_('Review status')}:</label>
93 </div>
94 <div class="input">
95 %if c.pull_request_review_status:
96 <div class="tag status-tag-${c.pull_request_review_status}">
97 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
98 <span class="changeset-status-lbl">
99 %if c.pull_request.is_closed():
100 ${_('Closed')},
101 %endif
102
103 ${h.commit_status_lbl(c.pull_request_review_status)}
104
105 </span>
106 </div>
107 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
108 %endif
109 </div>
110 </div>
111
112 ## source
113 <div class="field">
114 <div class="label-pr-detail">
115 <label>${_('Commit flow')}:</label>
116 </div>
117 <div class="input">
118 <div class="pr-commit-flow">
119 ## Source
120 %if c.pull_request.source_ref_parts.type == 'branch':
121 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
122 %else:
123 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
124 %endif
125 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
126 &rarr;
127 ## Target
128 %if c.pull_request.target_ref_parts.type == 'branch':
129 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
130 %else:
131 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
96 132 %endif
97 133
98 <div class="">
99 <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
100 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
101 </div>
134 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
135
136 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
137 <i class="icon-angle-down">more details</i>
138 </a>
102 139
103 140 </div>
104 </div>
105 <div class="field">
106 <div class="label-summary">
107 <label>${_('Target')}:</label>
108 </div>
109 <div class="input">
110 <div class="pr-targetinfo">
111 ## branch link is only valid if it is a branch
112 <span class="tag">
113 %if c.pull_request.target_ref_parts.type == 'branch':
114 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
115 %else:
116 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
117 %endif
118 </span>
119 <span class="clone-url">
120 <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
121 </span>
122 </div>
123 </div>
124 </div>
141
142 <div class="source-details" style="display: none">
143
144 <ul>
145
146 ## common ancestor
147 <li>
148 ${_('Common ancestor')}:
149 % if c.ancestor_commit:
150 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
151 % else:
152 ${_('not available')}
153 % endif
154 </li>
155
156 ## pull url
157 <li>
158 %if h.is_hg(c.pull_request.source_repo):
159 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
160 %elif h.is_git(c.pull_request.source_repo):
161 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
162 %endif
125 163
126 ## Link to the shadow repository.
127 <div class="field">
128 <div class="label-summary">
129 <label>${_('Merge')}:</label>
130 </div>
131 <div class="input">
132 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
133 %if h.is_hg(c.pull_request.target_repo):
134 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
135 %elif h.is_git(c.pull_request.target_repo):
136 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
137 %endif
138 <div class="">
139 <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
164 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
166 </li>
167
168 ## Shadow repo
169 <li>
170 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
171 %if h.is_hg(c.pull_request.target_repo):
172 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
173 %elif h.is_git(c.pull_request.target_repo):
174 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
175 %endif
176
177 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
140 178 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
141 </div>
142 % else:
143 <div class="">
144 ${_('Shadow repository data not available')}.
145 </div>
146 % endif
147 </div>
179
180 % else:
181 <div class="">
182 ${_('Shadow repository data not available')}.
183 </div>
184 % endif
185 </li>
186
187 </ul>
188
148 189 </div>
149 190
150 <div class="field">
151 <div class="label-summary">
152 <label>${_('Review')}:</label>
153 </div>
154 <div class="input">
155 %if c.pull_request_review_status:
156 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
157 <span class="changeset-status-lbl">
158 %if c.pull_request.is_closed():
159 ${_('Closed')},
160 %endif
161 ${h.commit_status_lbl(c.pull_request_review_status)}
162 </span>
163 - ${_ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
164 %endif
165 </div>
166 </div>
167 <div class="field">
168 <div class="pr-description-label label-summary" title="${_('Rendered using {} renderer').format(c.renderer)}">
169 <label>${_('Description')}:</label>
170 </div>
171 <div id="pr-desc" class="input">
172 <div class="pr-description">${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}</div>
173 </div>
174 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
175 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
176 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
177 </div>
191 </div>
192
193 </div>
194
195 ## versions
196 <div class="field">
197 <div class="label-pr-detail">
198 <label>${_('Versions')}:</label>
178 199 </div>
179 200
180 <div class="field">
181 <div class="label-summary">
182 <label>${_('Versions')}:</label>
183 </div>
184
185 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
186 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
201 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
202 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
187 203
188 <div class="pr-versions">
189 % if c.show_version_changes:
190 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
191 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
192 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
193 data-toggle-on="${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
194 data-toggle-off="${_('Hide all versions of this pull request')}">
195 ${_ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
196 </a>
197 <table>
198 ## SHOW ALL VERSIONS OF PR
199 <% ver_pr = None %>
204 <div class="pr-versions">
205 % if c.show_version_changes:
206 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
207 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
208 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
209 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
210 data-toggle-on="${_('show versions')}."
211 data-toggle-off="${_('hide versions')}.">
212 ${_('show versions')}.
213 </a>
214 <table>
215 ## SHOW ALL VERSIONS OF PR
216 <% ver_pr = None %>
200 217
201 % for data in reversed(list(enumerate(c.versions, 1))):
202 <% ver_pos = data[0] %>
203 <% ver = data[1] %>
204 <% ver_pr = ver.pull_request_version_id %>
205 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
218 % for data in reversed(list(enumerate(c.versions, 1))):
219 <% ver_pos = data[0] %>
220 <% ver = data[1] %>
221 <% ver_pr = ver.pull_request_version_id %>
222 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
206 223
207 <tr class="version-pr" style="display: ${display_row}">
208 <td>
209 <code>
210 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
211 </code>
212 </td>
213 <td>
214 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
215 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
216 </td>
217 <td>
218 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
219 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
224 <tr class="version-pr" style="display: ${display_row}">
225 <td>
226 <code>
227 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
228 </code>
229 </td>
230 <td>
231 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
232 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
233 </td>
234 <td>
235 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
236 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
220 237
221 </td>
222 <td>
223 % if c.at_version_num != ver_pr:
224 <i class="icon-comment"></i>
225 <code class="tooltip" title="${_('Comment from pull request version v{0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
226 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
227 </code>
228 % endif
229 </td>
230 <td>
231 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
232 </td>
233 <td>
234 ${h.age_component(ver.updated_on, time_is_local=True)}
235 </td>
236 </tr>
237 % endfor
238
239 <tr>
240 <td colspan="6">
241 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
242 data-label-text-locked="${_('select versions to show changes')}"
243 data-label-text-diff="${_('show changes between versions')}"
244 data-label-text-show="${_('show pull request for this version')}"
245 >
246 ${_('select versions to show changes')}
247 </button>
238 </td>
239 <td>
240 % if c.at_version_num != ver_pr:
241 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
242 <code>
243 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
244 </code>
245 % endif
246 </td>
247 <td>
248 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
249 </td>
250 <td>
251 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
248 252 </td>
249 253 </tr>
250 </table>
251 % else:
252 <div class="input">
253 ${_('Pull request versions not available')}.
254 </div>
255 % endif
254 % endfor
255
256 <tr>
257 <td colspan="6">
258 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
259 data-label-text-locked="${_('select versions to show changes')}"
260 data-label-text-diff="${_('show changes between versions')}"
261 data-label-text-show="${_('show pull request for this version')}"
262 >
263 ${_('select versions to show changes')}
264 </button>
265 </td>
266 </tr>
267 </table>
268 % else:
269 <div class="input">
270 ${_('Pull request versions not available')}.
256 271 </div>
272 % endif
257 273 </div>
274 </div>
258 275
259 <div id="pr-save" class="field" style="display: none;">
260 <div class="label-summary"></div>
261 <div class="input">
262 <span id="edit_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</span>
263 </div>
264 </div>
276 </div>
277
278 </div>
279
280 ## REVIEW RULES
281 <div id="review_rules" style="display: none" class="reviewers-title block-right">
282 <div class="pr-details-title">
283 ${_('Reviewer rules')}
284 %if c.allowed_to_update:
285 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
286 %endif
265 287 </div>
266 </div>
267 <div>
268 ## AUTHOR
269 <div class="reviewers-title block-right">
270 <div class="pr-details-title">
271 ${_('Author of this pull request')}
272 </div>
273 </div>
274 <div class="block-right pr-details-content reviewers">
275 <ul class="group_members">
276 <li>
277 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
278 </li>
279 </ul>
288 <div class="pr-reviewer-rules">
289 ## review rules will be appended here, by default reviewers logic
280 290 </div>
281
282 ## REVIEW RULES
283 <div id="review_rules" style="display: none" class="reviewers-title block-right">
284 <div class="pr-details-title">
285 ${_('Reviewer rules')}
286 %if c.allowed_to_update:
287 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
288 %endif
289 </div>
290 <div class="pr-reviewer-rules">
291 ## review rules will be appended here, by default reviewers logic
292 </div>
293 <input id="review_data" type="hidden" name="review_data" value="">
294 </div>
291 <input id="review_data" type="hidden" name="review_data" value="">
292 </div>
295 293
296 ## REVIEWERS
297 <div class="reviewers-title block-right">
298 <div class="pr-details-title">
299 ${_('Pull request reviewers')}
300 %if c.allowed_to_update:
301 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
302 %endif
303 </div>
304 </div>
305 <div id="reviewers" class="block-right pr-details-content reviewers">
294 ## REVIEWERS
295 <div class="reviewers-title block-right">
296 <div class="pr-details-title">
297 ${_('Pull request reviewers')}
298 %if c.allowed_to_update:
299 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
300 %endif
301 </div>
302 </div>
303 <div id="reviewers" class="block-right pr-details-content reviewers">
306 304
307 ## members redering block
308 <input type="hidden" name="__start__" value="review_members:sequence">
309 <ul id="review_members" class="group_members">
305 ## members redering block
306 <input type="hidden" name="__start__" value="review_members:sequence">
307 <ul id="review_members" class="group_members">
310 308
311 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
312 <script>
313 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
314 var status = "${(status[0][1].status if status else 'not_reviewed')}";
315 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
316 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
309 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
310 <script>
311 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
312 var status = "${(status[0][1].status if status else 'not_reviewed')}";
313 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
314 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
317 315
318 var entry = renderTemplate('reviewMemberEntry', {
319 'member': member,
320 'mandatory': member.mandatory,
321 'reasons': member.reasons,
322 'allowed_to_update': allowed_to_update,
323 'review_status': status,
324 'review_status_label': status_lbl,
325 'user_group': member.user_group,
326 'create': false
327 });
328 $('#review_members').append(entry)
329 </script>
316 var entry = renderTemplate('reviewMemberEntry', {
317 'member': member,
318 'mandatory': member.mandatory,
319 'reasons': member.reasons,
320 'allowed_to_update': allowed_to_update,
321 'review_status': status,
322 'review_status_label': status_lbl,
323 'user_group': member.user_group,
324 'create': false
325 });
326 $('#review_members').append(entry)
327 </script>
330 328
331 % endfor
332
333 </ul>
329 % endfor
334 330
335 <input type="hidden" name="__end__" value="review_members:sequence">
336 ## end members redering block
331 </ul>
332
333 <input type="hidden" name="__end__" value="review_members:sequence">
334 ## end members redering block
337 335
338 %if not c.pull_request.is_closed():
339 <div id="add_reviewer" class="ac" style="display: none;">
340 %if c.allowed_to_update:
341 % if not c.forbid_adding_reviewers:
342 <div id="add_reviewer_input" class="reviewer_ac">
343 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
344 <div id="reviewers_container"></div>
345 </div>
346 % endif
347 <div class="pull-right">
348 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
336 %if not c.pull_request.is_closed():
337 <div id="add_reviewer" class="ac" style="display: none;">
338 %if c.allowed_to_update:
339 % if not c.forbid_adding_reviewers:
340 <div id="add_reviewer_input" class="reviewer_ac">
341 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
342 <div id="reviewers_container"></div>
349 343 </div>
350 %endif
344 % endif
345 <div class="pull-right">
346 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
351 347 </div>
352 348 %endif
353 </div>
354 </div>
349 </div>
350 %endif
351 </div>
352
353 ## ## TODOs will be listed here
354 ## <div class="reviewers-title block-right">
355 ## <div class="pr-details-title">
356 ## ${_('TODOs')}
357 ## </div>
358 ## </div>
359 ## <div class="block-right pr-details-content reviewers">
360 ## <ul class="group_members">
361 ## <li>
362 ## XXXX
363 ## </li>
364 ## </ul>
365 ## </div>
366 ## </div>
367
355 368 </div>
356 369
357 370 <div class="box">
358 371
359 372 % if c.state_progressing:
373
360 374 <h2 style="text-align: center">
361 375 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
362 376 </h2>
363 377
364 378 % else:
365 379
366 380 ## Diffs rendered here
367 381 <div class="table" >
368 382 <div id="changeset_compare_view_content">
369 383 ##CS
370 384 % if c.missing_requirements:
371 385 <div class="box">
372 386 <div class="alert alert-warning">
373 387 <div>
374 388 <strong>${_('Missing requirements:')}</strong>
375 389 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
376 390 </div>
377 391 </div>
378 392 </div>
379 393 % elif c.missing_commits:
380 394 <div class="box">
381 395 <div class="alert alert-warning">
382 396 <div>
383 397 <strong>${_('Missing commits')}:</strong>
384 398 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
385 399 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
386 400 ${_('Consider doing a {force_refresh_url} in case you think this is an error.').format(force_refresh_url=h.link_to('force refresh', h.current_route_path(request, force_refresh='1')))|n}
387 401 </div>
388 402 </div>
389 403 </div>
390 404 % endif
391 405
392 406 <div class="compare_view_commits_title">
393 407 % if not c.compare_mode:
394 408
395 409 % if c.at_version_pos:
396 410 <h4>
397 411 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
398 412 </h4>
399 413 % endif
400 414
401 415 <div class="pull-left">
402 416 <div class="btn-group">
403 417 <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)} >
404 418 % if c.collapse_all_commits:
405 419 <i class="icon-plus-squared-alt icon-no-margin"></i>
406 420 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
407 421 % else:
408 422 <i class="icon-minus-squared-alt icon-no-margin"></i>
409 423 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
410 424 % endif
411 425 </a>
412 426 </div>
413 427 </div>
414 428
415 429 <div class="pull-right">
416 430 % if c.allowed_to_update and not c.pull_request.is_closed():
417 431
418 432 <div class="btn-group btn-group-actions">
419 433 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
420 434 ${_('Update commits')}
421 435 </a>
422 436
423 <a id="update_commits_switcher" class="btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button">
437 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
424 438 <i class="icon-down"></i>
425 439 </a>
426 440
427 441 <div class="btn-action-switcher-container" id="update-commits-switcher">
428 442 <ul class="btn-action-switcher" role="menu">
429 443 <li>
430 444 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
431 445 ${_('Force update commits')}
432 446 </a>
433 447 <div class="action-help-block">
434 448 ${_('Update commits and force refresh this pull request.')}
435 449 </div>
436 450 </li>
437 451 </ul>
438 452 </div>
439 453 </div>
440 454
441 455 % else:
442 456 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
443 457 % endif
444 458
445 459 </div>
446 460 % endif
447 461 </div>
448 462
449 463 % if not c.missing_commits:
450 464 % if c.compare_mode:
451 465 % if c.at_version:
452 466 <h4>
453 467 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
454 468 </h4>
455 469
456 470 <div class="subtitle-compare">
457 471 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
458 472 </div>
459 473
460 474 <div class="container">
461 475 <table class="rctable compare_view_commits">
462 476 <tr>
463 477 <th></th>
464 478 <th>${_('Time')}</th>
465 479 <th>${_('Author')}</th>
466 480 <th>${_('Commit')}</th>
467 481 <th></th>
468 482 <th>${_('Description')}</th>
469 483 </tr>
470 484
471 485 % for c_type, commit in c.commit_changes:
472 486 % if c_type in ['a', 'r']:
473 487 <%
474 488 if c_type == 'a':
475 489 cc_title = _('Commit added in displayed changes')
476 490 elif c_type == 'r':
477 491 cc_title = _('Commit removed in displayed changes')
478 492 else:
479 493 cc_title = ''
480 494 %>
481 495 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
482 496 <td>
483 497 <div class="commit-change-indicator color-${c_type}-border">
484 498 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
485 499 ${c_type.upper()}
486 500 </div>
487 501 </div>
488 502 </td>
489 503 <td class="td-time">
490 504 ${h.age_component(commit.date)}
491 505 </td>
492 506 <td class="td-user">
493 507 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
494 508 </td>
495 509 <td class="td-hash">
496 510 <code>
497 511 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
498 512 r${commit.idx}:${h.short_id(commit.raw_id)}
499 513 </a>
500 514 ${h.hidden('revisions', commit.raw_id)}
501 515 </code>
502 516 </td>
503 517 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
504 518 <i class="icon-expand-linked"></i>
505 519 </td>
506 520 <td class="mid td-description">
507 521 <div class="log-container truncate-wrap">
508 522 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
509 523 </div>
510 524 </td>
511 525 </tr>
512 526 % endif
513 527 % endfor
514 528 </table>
515 529 </div>
516 530
517 531 % endif
518 532
519 533 % else:
520 534 <%include file="/compare/compare_commits.mako" />
521 535 % endif
522 536
523 537 <div class="cs_files">
524 538 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
525 539 % if c.at_version:
526 540 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
527 541 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
528 542 % else:
529 543 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
530 544 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
531 545 % endif
532 546
533 547 <%
534 548 pr_menu_data = {
535 549 'outdated_comm_count_ver': outdated_comm_count_ver
536 550 }
537 551 %>
538 552
539 553 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
540 554
541 555 % if c.range_diff_on:
542 556 % for commit in c.commit_ranges:
543 557 ${cbdiffs.render_diffset(
544 558 c.changes[commit.raw_id],
545 559 commit=commit, use_comments=True,
546 560 collapse_when_files_over=5,
547 561 disable_new_comments=True,
548 562 deleted_files_comments=c.deleted_files_comments,
549 563 inline_comments=c.inline_comments,
550 564 pull_request_menu=pr_menu_data)}
551 565 % endfor
552 566 % else:
553 567 ${cbdiffs.render_diffset(
554 568 c.diffset, use_comments=True,
555 569 collapse_when_files_over=30,
556 570 disable_new_comments=not c.allowed_to_comment,
557 571 deleted_files_comments=c.deleted_files_comments,
558 572 inline_comments=c.inline_comments,
559 573 pull_request_menu=pr_menu_data)}
560 574 % endif
561 575
562 576 </div>
563 577 % else:
564 578 ## skipping commits we need to clear the view for missing commits
565 579 <div style="clear:both;"></div>
566 580 % endif
567 581
568 582 </div>
569 583 </div>
570 584
571 585 ## template for inline comment form
572 586 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
573 587
574 588 ## comments heading with count
575 589 <div class="comments-heading">
576 590 <i class="icon-comment"></i>
577 591 ${_('Comments')} ${len(c.comments)}
578 592 </div>
579 593
580 594 ## render general comments
581 595 <div id="comment-tr-show">
582 596 % if general_outdated_comm_count_ver:
583 597 <div class="info-box">
584 598 % if general_outdated_comm_count_ver == 1:
585 599 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
586 600 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
587 601 % else:
588 602 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
589 603 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
590 604 % endif
591 605 </div>
592 606 % endif
593 607 </div>
594 608
595 609 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
596 610
597 611 % if not c.pull_request.is_closed():
598 612 ## main comment form and it status
599 613 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
600 614 pull_request_id=c.pull_request.pull_request_id),
601 615 c.pull_request_review_status,
602 616 is_pull_request=True, change_status=c.allowed_to_change_status)}
603 617
604 618 ## merge status, and merge action
605 619 <div class="pull-request-merge">
606 620 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
607 621 </div>
608 622
609 623 %endif
610 624
611 625 % endif
612 626 </div>
613 627
614 <script type="text/javascript">
628 <script type="text/javascript">
615 629
616 versionController = new VersionController();
617 versionController.init();
630 versionController = new VersionController();
631 versionController.init();
618 632
619 reviewersController = new ReviewersController();
620 commitsController = new CommitsController();
633 reviewersController = new ReviewersController();
634 commitsController = new CommitsController();
621 635
622 updateController = new UpdatePrController();
636 updateController = new UpdatePrController();
623 637
624 $(function(){
638 $(function () {
625 639
626 // custom code mirror
627 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
640 // custom code mirror
641 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
628 642
629 var PRDetails = {
643 var PRDetails = {
630 644 editButton: $('#open_edit_pullrequest'),
631 645 closeButton: $('#close_edit_pullrequest'),
632 646 deleteButton: $('#delete_pullrequest'),
633 647 viewFields: $('#pr-desc, #pr-title'),
634 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
648 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
635 649
636 init: function() {
637 var that = this;
638 this.editButton.on('click', function(e) { that.edit(); });
639 this.closeButton.on('click', function(e) { that.view(); });
650 init: function () {
651 var that = this;
652 this.editButton.on('click', function (e) {
653 that.edit();
654 });
655 this.closeButton.on('click', function (e) {
656 that.view();
657 });
640 658 },
641 659
642 edit: function(event) {
643 this.viewFields.hide();
644 this.editButton.hide();
645 this.deleteButton.hide();
646 this.closeButton.show();
647 this.editFields.show();
648 codeMirrorInstance.refresh();
660 edit: function (event) {
661 this.viewFields.hide();
662 this.editButton.hide();
663 this.deleteButton.hide();
664 this.closeButton.show();
665 this.editFields.show();
666 codeMirrorInstance.refresh();
649 667 },
650 668
651 view: function(event) {
652 this.editButton.show();
653 this.deleteButton.show();
654 this.editFields.hide();
655 this.closeButton.hide();
656 this.viewFields.show();
669 view: function (event) {
670 this.editButton.show();
671 this.deleteButton.show();
672 this.editFields.hide();
673 this.closeButton.hide();
674 this.viewFields.show();
657 675 }
658 };
676 };
659 677
660 var ReviewersPanel = {
678 var ReviewersPanel = {
661 679 editButton: $('#open_edit_reviewers'),
662 680 closeButton: $('#close_edit_reviewers'),
663 681 addButton: $('#add_reviewer'),
664 682 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
665 683
666 init: function() {
667 var self = this;
668 this.editButton.on('click', function(e) { self.edit(); });
669 this.closeButton.on('click', function(e) { self.close(); });
684 init: function () {
685 var self = this;
686 this.editButton.on('click', function (e) {
687 self.edit();
688 });
689 this.closeButton.on('click', function (e) {
690 self.close();
691 });
670 692 },
671 693
672 edit: function(event) {
673 this.editButton.hide();
674 this.closeButton.show();
675 this.addButton.show();
676 this.removeButtons.css('visibility', 'visible');
677 // review rules
678 reviewersController.loadReviewRules(
679 ${c.pull_request.reviewer_data_json | n});
694 edit: function (event) {
695 this.editButton.hide();
696 this.closeButton.show();
697 this.addButton.show();
698 this.removeButtons.css('visibility', 'visible');
699 // review rules
700 reviewersController.loadReviewRules(
701 ${c.pull_request.reviewer_data_json | n});
680 702 },
681 703
682 close: function(event) {
683 this.editButton.show();
684 this.closeButton.hide();
685 this.addButton.hide();
686 this.removeButtons.css('visibility', 'hidden');
687 // hide review rules
688 reviewersController.hideReviewRules()
704 close: function (event) {
705 this.editButton.show();
706 this.closeButton.hide();
707 this.addButton.hide();
708 this.removeButtons.css('visibility', 'hidden');
709 // hide review rules
710 reviewersController.hideReviewRules()
689 711 }
690 };
712 };
691 713
692 PRDetails.init();
693 ReviewersPanel.init();
714 PRDetails.init();
715 ReviewersPanel.init();
694 716
695 showOutdated = function(self){
696 $('.comment-inline.comment-outdated').show();
697 $('.filediff-outdated').show();
698 $('.showOutdatedComments').hide();
699 $('.hideOutdatedComments').show();
700 };
717 showOutdated = function (self) {
718 $('.comment-inline.comment-outdated').show();
719 $('.filediff-outdated').show();
720 $('.showOutdatedComments').hide();
721 $('.hideOutdatedComments').show();
722 };
701 723
702 hideOutdated = function(self){
703 $('.comment-inline.comment-outdated').hide();
704 $('.filediff-outdated').hide();
705 $('.hideOutdatedComments').hide();
706 $('.showOutdatedComments').show();
707 };
724 hideOutdated = function (self) {
725 $('.comment-inline.comment-outdated').hide();
726 $('.filediff-outdated').hide();
727 $('.hideOutdatedComments').hide();
728 $('.showOutdatedComments').show();
729 };
708 730
709 refreshMergeChecks = function(){
710 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
711 $('.pull-request-merge').css('opacity', 0.3);
712 $('.action-buttons-extra').css('opacity', 0.3);
731 refreshMergeChecks = function () {
732 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
733 $('.pull-request-merge').css('opacity', 0.3);
734 $('.action-buttons-extra').css('opacity', 0.3);
713 735
714 $('.pull-request-merge').load(
715 loadUrl, function() {
716 $('.pull-request-merge').css('opacity', 1);
736 $('.pull-request-merge').load(
737 loadUrl, function () {
738 $('.pull-request-merge').css('opacity', 1);
717 739
718 $('.action-buttons-extra').css('opacity', 1);
719 }
720 );
721 };
740 $('.action-buttons-extra').css('opacity', 1);
741 }
742 );
743 };
722 744
723 closePullRequest = function (status) {
724 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
745 closePullRequest = function (status) {
746 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
725 747 return false;
726 }
727 // inject closing flag
728 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
729 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
730 $(generalCommentForm.submitForm).submit();
731 };
748 }
749 // inject closing flag
750 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
751 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
752 $(generalCommentForm.submitForm).submit();
753 };
732 754
733 $('#show-outdated-comments').on('click', function(e){
734 var button = $(this);
735 var outdated = $('.comment-outdated');
755 $('#show-outdated-comments').on('click', function (e) {
756 var button = $(this);
757 var outdated = $('.comment-outdated');
736 758
737 if (button.html() === "(Show)") {
759 if (button.html() === "(Show)") {
738 760 button.html("(Hide)");
739 761 outdated.show();
740 } else {
762 } else {
741 763 button.html("(Show)");
742 764 outdated.hide();
743 }
744 });
765 }
766 });
745 767
746 $('.show-inline-comments').on('change', function(e){
747 var show = 'none';
748 var target = e.currentTarget;
749 if(target.checked){
750 show = ''
751 }
752 var boxid = $(target).attr('id_for');
753 var comments = $('#{0} .inline-comments'.format(boxid));
754 var fn_display = function(idx){
755 $(this).css('display', show);
756 };
757 $(comments).each(fn_display);
758 var btns = $('#{0} .inline-comments-button'.format(boxid));
759 $(btns).each(fn_display);
760 });
768 $('.show-inline-comments').on('change', function (e) {
769 var show = 'none';
770 var target = e.currentTarget;
771 if (target.checked) {
772 show = ''
773 }
774 var boxid = $(target).attr('id_for');
775 var comments = $('#{0} .inline-comments'.format(boxid));
776 var fn_display = function (idx) {
777 $(this).css('display', show);
778 };
779 $(comments).each(fn_display);
780 var btns = $('#{0} .inline-comments-button'.format(boxid));
781 $(btns).each(fn_display);
782 });
761 783
762 $('#merge_pull_request_form').submit(function() {
763 if (!$('#merge_pull_request').attr('disabled')) {
764 $('#merge_pull_request').attr('disabled', 'disabled');
765 }
766 return true;
767 });
784 $('#merge_pull_request_form').submit(function () {
785 if (!$('#merge_pull_request').attr('disabled')) {
786 $('#merge_pull_request').attr('disabled', 'disabled');
787 }
788 return true;
789 });
768 790
769 $('#edit_pull_request').on('click', function(e){
770 var title = $('#pr-title-input').val();
771 var description = codeMirrorInstance.getValue();
772 var renderer = $('#pr-renderer-input').val();
773 editPullRequest(
774 "${c.repo_name}", "${c.pull_request.pull_request_id}",
775 title, description, renderer);
776 });
791 $('#edit_pull_request').on('click', function (e) {
792 var title = $('#pr-title-input').val();
793 var description = codeMirrorInstance.getValue();
794 var renderer = $('#pr-renderer-input').val();
795 editPullRequest(
796 "${c.repo_name}", "${c.pull_request.pull_request_id}",
797 title, description, renderer);
798 });
777 799
778 $('#update_pull_request').on('click', function(e){
779 $(this).attr('disabled', 'disabled');
780 $(this).addClass('disabled');
781 $(this).html(_gettext('Saving...'));
782 reviewersController.updateReviewers(
783 "${c.repo_name}", "${c.pull_request.pull_request_id}");
784 });
800 $('#update_pull_request').on('click', function (e) {
801 $(this).attr('disabled', 'disabled');
802 $(this).addClass('disabled');
803 $(this).html(_gettext('Saving...'));
804 reviewersController.updateReviewers(
805 "${c.repo_name}", "${c.pull_request.pull_request_id}");
806 });
785 807
786 808
787 // fixing issue with caches on firefox
788 $('#update_commits').removeAttr("disabled");
809 // fixing issue with caches on firefox
810 $('#update_commits').removeAttr("disabled");
789 811
790 $('.show-inline-comments').on('click', function(e){
791 var boxid = $(this).attr('data-comment-id');
792 var button = $(this);
812 $('.show-inline-comments').on('click', function (e) {
813 var boxid = $(this).attr('data-comment-id');
814 var button = $(this);
793 815
794 if(button.hasClass("comments-visible")) {
795 $('#{0} .inline-comments'.format(boxid)).each(function(index){
796 $(this).hide();
816 if (button.hasClass("comments-visible")) {
817 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
818 $(this).hide();
797 819 });
798 820 button.removeClass("comments-visible");
799 } else {
800 $('#{0} .inline-comments'.format(boxid)).each(function(index){
801 $(this).show();
821 } else {
822 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
823 $(this).show();
802 824 });
803 825 button.addClass("comments-visible");
804 }
805 });
826 }
827 });
806 828
807 // register submit callback on commentForm form to track TODOs
808 window.commentFormGlobalSubmitSuccessCallback = function(){
809 refreshMergeChecks();
810 };
829 // register submit callback on commentForm form to track TODOs
830 window.commentFormGlobalSubmitSuccessCallback = function () {
831 refreshMergeChecks();
832 };
811 833
812 ReviewerAutoComplete('#user');
834 ReviewerAutoComplete('#user');
813 835
814 })
836 })
815 837
816 </script>
838 </script>
839
817 840 </div>
818 841
819 842 </%def>
General Comments 0
You need to be logged in to leave comments. Login now