##// END OF EJS Templates
comments: multiple changes on comments navigation/display logic...
milka -
r4543:624997f0 default
parent child Browse files
Show More

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

@@ -1,486 +1,486 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import logging
22 import logging
23 import datetime
23 import datetime
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render_to_response
26 from pyramid.renderers import render_to_response
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
31 from rhodecode.model.notification import EmailNotificationModel
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37
37
38 def load_default_context(self):
38 def load_default_context(self):
39 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
40
40
41 return c
41 return c
42
42
43 @view_config(
43 @view_config(
44 route_name='debug_style_home', request_method='GET',
44 route_name='debug_style_home', request_method='GET',
45 renderer=None)
45 renderer=None)
46 def index(self):
46 def index(self):
47 c = self.load_default_context()
47 c = self.load_default_context()
48 c.active = 'index'
48 c.active = 'index'
49
49
50 return render_to_response(
50 return render_to_response(
51 'debug_style/index.html', self._get_template_context(c),
51 'debug_style/index.html', self._get_template_context(c),
52 request=self.request)
52 request=self.request)
53
53
54 @view_config(
54 @view_config(
55 route_name='debug_style_email', request_method='GET',
55 route_name='debug_style_email', request_method='GET',
56 renderer=None)
56 renderer=None)
57 @view_config(
57 @view_config(
58 route_name='debug_style_email_plain_rendered', request_method='GET',
58 route_name='debug_style_email_plain_rendered', request_method='GET',
59 renderer=None)
59 renderer=None)
60 def render_email(self):
60 def render_email(self):
61 c = self.load_default_context()
61 c = self.load_default_context()
62 email_id = self.request.matchdict['email_id']
62 email_id = self.request.matchdict['email_id']
63 c.active = 'emails'
63 c.active = 'emails'
64
64
65 pr = AttributeDict(
65 pr = AttributeDict(
66 pull_request_id=123,
66 pull_request_id=123,
67 title='digital_ocean: fix redis, elastic search start on boot, '
67 title='digital_ocean: fix redis, elastic search start on boot, '
68 'fix fd limits on supervisor, set postgres 11 version',
68 'fix fd limits on supervisor, set postgres 11 version',
69 description='''
69 description='''
70 Check if we should use full-topic or mini-topic.
70 Check if we should use full-topic or mini-topic.
71
71
72 - full topic produces some problems with merge states etc
72 - full topic produces some problems with merge states etc
73 - server-mini-topic needs probably tweeks.
73 - server-mini-topic needs probably tweeks.
74 ''',
74 ''',
75 repo_name='foobar',
75 repo_name='foobar',
76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
77 target_ref_parts=AttributeDict(type='branch', name='master'),
77 target_ref_parts=AttributeDict(type='branch', name='master'),
78 )
78 )
79
79
80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
83 # file/commit changes for PR update
83 # file/commit changes for PR update
84 commit_changes = AttributeDict({
84 commit_changes = AttributeDict({
85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
86 'removed': ['eeeeeeeeeee'],
86 'removed': ['eeeeeeeeeee'],
87 })
87 })
88
88
89 file_changes = AttributeDict({
89 file_changes = AttributeDict({
90 'added': ['a/file1.md', 'file2.py'],
90 'added': ['a/file1.md', 'file2.py'],
91 'modified': ['b/modified_file.rst'],
91 'modified': ['b/modified_file.rst'],
92 'removed': ['.idea'],
92 'removed': ['.idea'],
93 })
93 })
94
94
95 exc_traceback = {
95 exc_traceback = {
96 'exc_utc_date': '2020-03-26T12:54:50.683281',
96 'exc_utc_date': '2020-03-26T12:54:50.683281',
97 'exc_id': 139638856342656,
97 'exc_id': 139638856342656,
98 'exc_timestamp': '1585227290.683288',
98 'exc_timestamp': '1585227290.683288',
99 'version': 'v1',
99 'version': 'v1',
100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
101 'exc_type': 'AttributeError'
101 'exc_type': 'AttributeError'
102 }
102 }
103
103
104 email_kwargs = {
104 email_kwargs = {
105 'test': {},
105 'test': {},
106
106
107 'message': {
107 'message': {
108 'body': 'message body !'
108 'body': 'message body !'
109 },
109 },
110
110
111 'email_test': {
111 'email_test': {
112 'user': user,
112 'user': user,
113 'date': datetime.datetime.now(),
113 'date': datetime.datetime.now(),
114 },
114 },
115
115
116 'exception': {
116 'exception': {
117 'email_prefix': '[RHODECODE ERROR]',
117 'email_prefix': '[RHODECODE ERROR]',
118 'exc_id': exc_traceback['exc_id'],
118 'exc_id': exc_traceback['exc_id'],
119 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
119 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
120 'exc_type_name': 'NameError',
120 'exc_type_name': 'NameError',
121 'exc_traceback': exc_traceback,
121 'exc_traceback': exc_traceback,
122 },
122 },
123
123
124 'password_reset': {
124 'password_reset': {
125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
126
126
127 'user': user,
127 'user': user,
128 'date': datetime.datetime.now(),
128 'date': datetime.datetime.now(),
129 'email': 'test@rhodecode.com',
129 'email': 'test@rhodecode.com',
130 'first_admin_email': User.get_first_super_admin().email
130 'first_admin_email': User.get_first_super_admin().email
131 },
131 },
132
132
133 'password_reset_confirmation': {
133 'password_reset_confirmation': {
134 'new_password': 'new-password-example',
134 'new_password': 'new-password-example',
135 'user': user,
135 'user': user,
136 'date': datetime.datetime.now(),
136 'date': datetime.datetime.now(),
137 'email': 'test@rhodecode.com',
137 'email': 'test@rhodecode.com',
138 'first_admin_email': User.get_first_super_admin().email
138 'first_admin_email': User.get_first_super_admin().email
139 },
139 },
140
140
141 'registration': {
141 'registration': {
142 'user': user,
142 'user': user,
143 'date': datetime.datetime.now(),
143 'date': datetime.datetime.now(),
144 },
144 },
145
145
146 'pull_request_comment': {
146 'pull_request_comment': {
147 'user': user,
147 'user': user,
148
148
149 'status_change': None,
149 'status_change': None,
150 'status_change_type': None,
150 'status_change_type': None,
151
151
152 'pull_request': pr,
152 'pull_request': pr,
153 'pull_request_commits': [],
153 'pull_request_commits': [],
154
154
155 'pull_request_target_repo': target_repo,
155 'pull_request_target_repo': target_repo,
156 'pull_request_target_repo_url': 'http://target-repo/url',
156 'pull_request_target_repo_url': 'http://target-repo/url',
157
157
158 'pull_request_source_repo': source_repo,
158 'pull_request_source_repo': source_repo,
159 'pull_request_source_repo_url': 'http://source-repo/url',
159 'pull_request_source_repo_url': 'http://source-repo/url',
160
160
161 'pull_request_url': 'http://localhost/pr1',
161 'pull_request_url': 'http://localhost/pr1',
162 'pr_comment_url': 'http://comment-url',
162 'pr_comment_url': 'http://comment-url',
163 'pr_comment_reply_url': 'http://comment-url#reply',
163 'pr_comment_reply_url': 'http://comment-url#reply',
164
164
165 'comment_file': None,
165 'comment_file': None,
166 'comment_line': None,
166 'comment_line': None,
167 'comment_type': 'note',
167 'comment_type': 'note',
168 'comment_body': 'This is my comment body. *I like !*',
168 'comment_body': 'This is my comment body. *I like !*',
169 'comment_id': 2048,
169 'comment_id': 2048,
170 'renderer_type': 'markdown',
170 'renderer_type': 'markdown',
171 'mention': True,
171 'mention': True,
172
172
173 },
173 },
174
174
175 'pull_request_comment+status': {
175 'pull_request_comment+status': {
176 'user': user,
176 'user': user,
177
177
178 'status_change': 'approved',
178 'status_change': 'approved',
179 'status_change_type': 'approved',
179 'status_change_type': 'approved',
180
180
181 'pull_request': pr,
181 'pull_request': pr,
182 'pull_request_commits': [],
182 'pull_request_commits': [],
183
183
184 'pull_request_target_repo': target_repo,
184 'pull_request_target_repo': target_repo,
185 'pull_request_target_repo_url': 'http://target-repo/url',
185 'pull_request_target_repo_url': 'http://target-repo/url',
186
186
187 'pull_request_source_repo': source_repo,
187 'pull_request_source_repo': source_repo,
188 'pull_request_source_repo_url': 'http://source-repo/url',
188 'pull_request_source_repo_url': 'http://source-repo/url',
189
189
190 'pull_request_url': 'http://localhost/pr1',
190 'pull_request_url': 'http://localhost/pr1',
191 'pr_comment_url': 'http://comment-url',
191 'pr_comment_url': 'http://comment-url',
192 'pr_comment_reply_url': 'http://comment-url#reply',
192 'pr_comment_reply_url': 'http://comment-url#reply',
193
193
194 'comment_type': 'todo',
194 'comment_type': 'todo',
195 'comment_file': None,
195 'comment_file': None,
196 'comment_line': None,
196 'comment_line': None,
197 'comment_body': '''
197 'comment_body': '''
198 I think something like this would be better
198 I think something like this would be better
199
199
200 ```py
200 ```py
201 // markdown renderer
201 // markdown renderer
202
202
203 def db():
203 def db():
204 global connection
204 global connection
205 return connection
205 return connection
206
206
207 ```
207 ```
208
208
209 ''',
209 ''',
210 'comment_id': 2048,
210 'comment_id': 2048,
211 'renderer_type': 'markdown',
211 'renderer_type': 'markdown',
212 'mention': True,
212 'mention': True,
213
213
214 },
214 },
215
215
216 'pull_request_comment+file': {
216 'pull_request_comment+file': {
217 'user': user,
217 'user': user,
218
218
219 'status_change': None,
219 'status_change': None,
220 'status_change_type': None,
220 'status_change_type': None,
221
221
222 'pull_request': pr,
222 'pull_request': pr,
223 'pull_request_commits': [],
223 'pull_request_commits': [],
224
224
225 'pull_request_target_repo': target_repo,
225 'pull_request_target_repo': target_repo,
226 'pull_request_target_repo_url': 'http://target-repo/url',
226 'pull_request_target_repo_url': 'http://target-repo/url',
227
227
228 'pull_request_source_repo': source_repo,
228 'pull_request_source_repo': source_repo,
229 'pull_request_source_repo_url': 'http://source-repo/url',
229 'pull_request_source_repo_url': 'http://source-repo/url',
230
230
231 'pull_request_url': 'http://localhost/pr1',
231 'pull_request_url': 'http://localhost/pr1',
232
232
233 'pr_comment_url': 'http://comment-url',
233 'pr_comment_url': 'http://comment-url',
234 'pr_comment_reply_url': 'http://comment-url#reply',
234 'pr_comment_reply_url': 'http://comment-url#reply',
235
235
236 'comment_file': 'rhodecode/model/get_flow_commits',
236 'comment_file': 'rhodecode/model/get_flow_commits',
237 'comment_line': 'o1210',
237 'comment_line': 'o1210',
238 'comment_type': 'todo',
238 'comment_type': 'todo',
239 'comment_body': '''
239 'comment_body': '''
240 I like this !
240 I like this !
241
241
242 But please check this code
242 But please check this code
243
243
244 .. code-block:: javascript
244 .. code-block:: javascript
245
245
246 // THIS IS RST CODE
246 // THIS IS RST CODE
247
247
248 this.createResolutionComment = function(commentId) {
248 this.createResolutionComment = function(commentId) {
249 // hide the trigger text
249 // hide the trigger text
250 $('#resolve-comment-{0}'.format(commentId)).hide();
250 $('#resolve-comment-{0}'.format(commentId)).hide();
251
251
252 var comment = $('#comment-'+commentId);
252 var comment = $('#comment-'+commentId);
253 var commentData = comment.data();
253 var commentData = comment.data();
254 if (commentData.commentInline) {
254 if (commentData.commentInline) {
255 this.createComment(comment, commentId)
255 this.createComment(comment, f_path, line_no, commentId)
256 } else {
256 } else {
257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
257 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
258 }
258 }
259
259
260 return false;
260 return false;
261 };
261 };
262
262
263 This should work better !
263 This should work better !
264 ''',
264 ''',
265 'comment_id': 2048,
265 'comment_id': 2048,
266 'renderer_type': 'rst',
266 'renderer_type': 'rst',
267 'mention': True,
267 'mention': True,
268
268
269 },
269 },
270
270
271 'pull_request_update': {
271 'pull_request_update': {
272 'updating_user': user,
272 'updating_user': user,
273
273
274 'status_change': None,
274 'status_change': None,
275 'status_change_type': None,
275 'status_change_type': None,
276
276
277 'pull_request': pr,
277 'pull_request': pr,
278 'pull_request_commits': [],
278 'pull_request_commits': [],
279
279
280 'pull_request_target_repo': target_repo,
280 'pull_request_target_repo': target_repo,
281 'pull_request_target_repo_url': 'http://target-repo/url',
281 'pull_request_target_repo_url': 'http://target-repo/url',
282
282
283 'pull_request_source_repo': source_repo,
283 'pull_request_source_repo': source_repo,
284 'pull_request_source_repo_url': 'http://source-repo/url',
284 'pull_request_source_repo_url': 'http://source-repo/url',
285
285
286 'pull_request_url': 'http://localhost/pr1',
286 'pull_request_url': 'http://localhost/pr1',
287
287
288 # update comment links
288 # update comment links
289 'pr_comment_url': 'http://comment-url',
289 'pr_comment_url': 'http://comment-url',
290 'pr_comment_reply_url': 'http://comment-url#reply',
290 'pr_comment_reply_url': 'http://comment-url#reply',
291 'ancestor_commit_id': 'f39bd443',
291 'ancestor_commit_id': 'f39bd443',
292 'added_commits': commit_changes.added,
292 'added_commits': commit_changes.added,
293 'removed_commits': commit_changes.removed,
293 'removed_commits': commit_changes.removed,
294 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
294 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
295 'added_files': file_changes.added,
295 'added_files': file_changes.added,
296 'modified_files': file_changes.modified,
296 'modified_files': file_changes.modified,
297 'removed_files': file_changes.removed,
297 'removed_files': file_changes.removed,
298 },
298 },
299
299
300 'cs_comment': {
300 'cs_comment': {
301 'user': user,
301 'user': user,
302 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
302 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
303 'status_change': None,
303 'status_change': None,
304 'status_change_type': None,
304 'status_change_type': None,
305
305
306 'commit_target_repo_url': 'http://foo.example.com/#comment1',
306 'commit_target_repo_url': 'http://foo.example.com/#comment1',
307 'repo_name': 'test-repo',
307 'repo_name': 'test-repo',
308 'comment_type': 'note',
308 'comment_type': 'note',
309 'comment_file': None,
309 'comment_file': None,
310 'comment_line': None,
310 'comment_line': None,
311 'commit_comment_url': 'http://comment-url',
311 'commit_comment_url': 'http://comment-url',
312 'commit_comment_reply_url': 'http://comment-url#reply',
312 'commit_comment_reply_url': 'http://comment-url#reply',
313 'comment_body': 'This is my comment body. *I like !*',
313 'comment_body': 'This is my comment body. *I like !*',
314 'comment_id': 2048,
314 'comment_id': 2048,
315 'renderer_type': 'markdown',
315 'renderer_type': 'markdown',
316 'mention': True,
316 'mention': True,
317 },
317 },
318
318
319 'cs_comment+status': {
319 'cs_comment+status': {
320 'user': user,
320 'user': user,
321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
322 'status_change': 'approved',
322 'status_change': 'approved',
323 'status_change_type': 'approved',
323 'status_change_type': 'approved',
324
324
325 'commit_target_repo_url': 'http://foo.example.com/#comment1',
325 'commit_target_repo_url': 'http://foo.example.com/#comment1',
326 'repo_name': 'test-repo',
326 'repo_name': 'test-repo',
327 'comment_type': 'note',
327 'comment_type': 'note',
328 'comment_file': None,
328 'comment_file': None,
329 'comment_line': None,
329 'comment_line': None,
330 'commit_comment_url': 'http://comment-url',
330 'commit_comment_url': 'http://comment-url',
331 'commit_comment_reply_url': 'http://comment-url#reply',
331 'commit_comment_reply_url': 'http://comment-url#reply',
332 'comment_body': '''
332 'comment_body': '''
333 Hello **world**
333 Hello **world**
334
334
335 This is a multiline comment :)
335 This is a multiline comment :)
336
336
337 - list
337 - list
338 - list2
338 - list2
339 ''',
339 ''',
340 'comment_id': 2048,
340 'comment_id': 2048,
341 'renderer_type': 'markdown',
341 'renderer_type': 'markdown',
342 'mention': True,
342 'mention': True,
343 },
343 },
344
344
345 'cs_comment+file': {
345 'cs_comment+file': {
346 'user': user,
346 'user': user,
347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
348 'status_change': None,
348 'status_change': None,
349 'status_change_type': None,
349 'status_change_type': None,
350
350
351 'commit_target_repo_url': 'http://foo.example.com/#comment1',
351 'commit_target_repo_url': 'http://foo.example.com/#comment1',
352 'repo_name': 'test-repo',
352 'repo_name': 'test-repo',
353
353
354 'comment_type': 'note',
354 'comment_type': 'note',
355 'comment_file': 'test-file.py',
355 'comment_file': 'test-file.py',
356 'comment_line': 'n100',
356 'comment_line': 'n100',
357
357
358 'commit_comment_url': 'http://comment-url',
358 'commit_comment_url': 'http://comment-url',
359 'commit_comment_reply_url': 'http://comment-url#reply',
359 'commit_comment_reply_url': 'http://comment-url#reply',
360 'comment_body': 'This is my comment body. *I like !*',
360 'comment_body': 'This is my comment body. *I like !*',
361 'comment_id': 2048,
361 'comment_id': 2048,
362 'renderer_type': 'markdown',
362 'renderer_type': 'markdown',
363 'mention': True,
363 'mention': True,
364 },
364 },
365
365
366 'pull_request': {
366 'pull_request': {
367 'user': user,
367 'user': user,
368 'pull_request': pr,
368 'pull_request': pr,
369 'pull_request_commits': [
369 'pull_request_commits': [
370 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
370 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
371 my-account: moved email closer to profile as it's similar data just moved outside.
371 my-account: moved email closer to profile as it's similar data just moved outside.
372 '''),
372 '''),
373 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
373 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
374 users: description edit fixes
374 users: description edit fixes
375
375
376 - tests
376 - tests
377 - added metatags info
377 - added metatags info
378 '''),
378 '''),
379 ],
379 ],
380
380
381 'pull_request_target_repo': target_repo,
381 'pull_request_target_repo': target_repo,
382 'pull_request_target_repo_url': 'http://target-repo/url',
382 'pull_request_target_repo_url': 'http://target-repo/url',
383
383
384 'pull_request_source_repo': source_repo,
384 'pull_request_source_repo': source_repo,
385 'pull_request_source_repo_url': 'http://source-repo/url',
385 'pull_request_source_repo_url': 'http://source-repo/url',
386
386
387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 'user_role': 'reviewer',
388 'user_role': 'reviewer',
389 },
389 },
390
390
391 'pull_request+reviewer_role': {
391 'pull_request+reviewer_role': {
392 'user': user,
392 'user': user,
393 'pull_request': pr,
393 'pull_request': pr,
394 'pull_request_commits': [
394 'pull_request_commits': [
395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
396 my-account: moved email closer to profile as it's similar data just moved outside.
396 my-account: moved email closer to profile as it's similar data just moved outside.
397 '''),
397 '''),
398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
399 users: description edit fixes
399 users: description edit fixes
400
400
401 - tests
401 - tests
402 - added metatags info
402 - added metatags info
403 '''),
403 '''),
404 ],
404 ],
405
405
406 'pull_request_target_repo': target_repo,
406 'pull_request_target_repo': target_repo,
407 'pull_request_target_repo_url': 'http://target-repo/url',
407 'pull_request_target_repo_url': 'http://target-repo/url',
408
408
409 'pull_request_source_repo': source_repo,
409 'pull_request_source_repo': source_repo,
410 'pull_request_source_repo_url': 'http://source-repo/url',
410 'pull_request_source_repo_url': 'http://source-repo/url',
411
411
412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 'user_role': 'reviewer',
413 'user_role': 'reviewer',
414 },
414 },
415
415
416 'pull_request+observer_role': {
416 'pull_request+observer_role': {
417 'user': user,
417 'user': user,
418 'pull_request': pr,
418 'pull_request': pr,
419 'pull_request_commits': [
419 'pull_request_commits': [
420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 my-account: moved email closer to profile as it's similar data just moved outside.
421 my-account: moved email closer to profile as it's similar data just moved outside.
422 '''),
422 '''),
423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 users: description edit fixes
424 users: description edit fixes
425
425
426 - tests
426 - tests
427 - added metatags info
427 - added metatags info
428 '''),
428 '''),
429 ],
429 ],
430
430
431 'pull_request_target_repo': target_repo,
431 'pull_request_target_repo': target_repo,
432 'pull_request_target_repo_url': 'http://target-repo/url',
432 'pull_request_target_repo_url': 'http://target-repo/url',
433
433
434 'pull_request_source_repo': source_repo,
434 'pull_request_source_repo': source_repo,
435 'pull_request_source_repo_url': 'http://source-repo/url',
435 'pull_request_source_repo_url': 'http://source-repo/url',
436
436
437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 'user_role': 'observer'
438 'user_role': 'observer'
439 }
439 }
440 }
440 }
441
441
442 template_type = email_id.split('+')[0]
442 template_type = email_id.split('+')[0]
443 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
443 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
444 template_type, **email_kwargs.get(email_id, {}))
444 template_type, **email_kwargs.get(email_id, {}))
445
445
446 test_email = self.request.GET.get('email')
446 test_email = self.request.GET.get('email')
447 if test_email:
447 if test_email:
448 recipients = [test_email]
448 recipients = [test_email]
449 run_task(tasks.send_email, recipients, c.subject,
449 run_task(tasks.send_email, recipients, c.subject,
450 c.email_body_plaintext, c.email_body)
450 c.email_body_plaintext, c.email_body)
451
451
452 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
452 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
453 template = 'debug_style/email_plain_rendered.mako'
453 template = 'debug_style/email_plain_rendered.mako'
454 else:
454 else:
455 template = 'debug_style/email.mako'
455 template = 'debug_style/email.mako'
456 return render_to_response(
456 return render_to_response(
457 template, self._get_template_context(c),
457 template, self._get_template_context(c),
458 request=self.request)
458 request=self.request)
459
459
460 @view_config(
460 @view_config(
461 route_name='debug_style_template', request_method='GET',
461 route_name='debug_style_template', request_method='GET',
462 renderer=None)
462 renderer=None)
463 def template(self):
463 def template(self):
464 t_path = self.request.matchdict['t_path']
464 t_path = self.request.matchdict['t_path']
465 c = self.load_default_context()
465 c = self.load_default_context()
466 c.active = os.path.splitext(t_path)[0]
466 c.active = os.path.splitext(t_path)[0]
467 c.came_from = ''
467 c.came_from = ''
468 # NOTE(marcink): extend the email types with variations based on data sets
468 # NOTE(marcink): extend the email types with variations based on data sets
469 c.email_types = {
469 c.email_types = {
470 'cs_comment+file': {},
470 'cs_comment+file': {},
471 'cs_comment+status': {},
471 'cs_comment+status': {},
472
472
473 'pull_request_comment+file': {},
473 'pull_request_comment+file': {},
474 'pull_request_comment+status': {},
474 'pull_request_comment+status': {},
475
475
476 'pull_request_update': {},
476 'pull_request_update': {},
477
477
478 'pull_request+reviewer_role': {},
478 'pull_request+reviewer_role': {},
479 'pull_request+observer_role': {},
479 'pull_request+observer_role': {},
480 }
480 }
481 c.email_types.update(EmailNotificationModel.email_types)
481 c.email_types.update(EmailNotificationModel.email_types)
482
482
483 return render_to_response(
483 return render_to_response(
484 'debug_style/' + t_path, self._get_template_context(c),
484 'debug_style/' + t_path, self._get_template_context(c),
485 request=self.request)
485 request=self.request)
486
486
@@ -1,791 +1,795 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks, channelstream
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
44 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 c.changes = OrderedDict()
120 c.changes = OrderedDict()
121 c.lines_added = 0
121 c.lines_added = 0
122 c.lines_deleted = 0
122 c.lines_deleted = 0
123
123
124 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127
127
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
129 c.inline_comments = []
129 c.inline_comments = []
130 c.files = []
130 c.files = []
131
131
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135
135
136 # Single commit
136 # Single commit
137 if single_commit:
137 if single_commit:
138 commit = c.commit_ranges[0]
138 commit = c.commit_ranges[0]
139 c.comments = CommentsModel().get_comments(
139 c.comments = CommentsModel().get_comments(
140 self.db_repo.repo_id,
140 self.db_repo.repo_id,
141 revision=commit.raw_id)
141 revision=commit.raw_id)
142
142
143 # comments from PR
143 # comments from PR
144 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
145 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
146 with_revisions=True)
146 with_revisions=True)
147
147
148 prs = set()
148 prs = set()
149 reviewers = list()
149 reviewers = list()
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
150 reviewers_duplicates = set() # to not have duplicates from multiple votes
151 for c_status in statuses:
151 for c_status in statuses:
152
152
153 # extract associated pull-requests from votes
153 # extract associated pull-requests from votes
154 if c_status.pull_request:
154 if c_status.pull_request:
155 prs.add(c_status.pull_request)
155 prs.add(c_status.pull_request)
156
156
157 # extract reviewers
157 # extract reviewers
158 _user_id = c_status.author.user_id
158 _user_id = c_status.author.user_id
159 if _user_id not in reviewers_duplicates:
159 if _user_id not in reviewers_duplicates:
160 reviewers.append(
160 reviewers.append(
161 StrictAttributeDict({
161 StrictAttributeDict({
162 'user': c_status.author,
162 'user': c_status.author,
163
163
164 # fake attributed for commit, page that we don't have
164 # fake attributed for commit, page that we don't have
165 # but we share the display with PR page
165 # but we share the display with PR page
166 'mandatory': False,
166 'mandatory': False,
167 'reasons': [],
167 'reasons': [],
168 'rule_user_group_data': lambda: None
168 'rule_user_group_data': lambda: None
169 })
169 })
170 )
170 )
171 reviewers_duplicates.add(_user_id)
171 reviewers_duplicates.add(_user_id)
172
172
173 c.reviewers_count = len(reviewers)
173 c.reviewers_count = len(reviewers)
174 c.observers_count = 0
174 c.observers_count = 0
175
175
176 # from associated statuses, check the pull requests, and
176 # from associated statuses, check the pull requests, and
177 # show comments from them
177 # show comments from them
178 for pr in prs:
178 for pr in prs:
179 c.comments.extend(pr.comments)
179 c.comments.extend(pr.comments)
180
180
181 c.unresolved_comments = CommentsModel()\
181 c.unresolved_comments = CommentsModel()\
182 .get_commit_unresolved_todos(commit.raw_id)
182 .get_commit_unresolved_todos(commit.raw_id)
183 c.resolved_comments = CommentsModel()\
183 c.resolved_comments = CommentsModel()\
184 .get_commit_resolved_todos(commit.raw_id)
184 .get_commit_resolved_todos(commit.raw_id)
185
185
186 c.inline_comments_flat = CommentsModel()\
186 c.inline_comments_flat = CommentsModel()\
187 .get_commit_inline_comments(commit.raw_id)
187 .get_commit_inline_comments(commit.raw_id)
188
188
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
189 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
190 statuses, reviewers)
190 statuses, reviewers)
191
191
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
192 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
193
193
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
194 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
195
195
196 for review_obj, member, reasons, mandatory, status in review_statuses:
196 for review_obj, member, reasons, mandatory, status in review_statuses:
197 member_reviewer = h.reviewer_as_json(
197 member_reviewer = h.reviewer_as_json(
198 member, reasons=reasons, mandatory=mandatory, role=None,
198 member, reasons=reasons, mandatory=mandatory, role=None,
199 user_group=None
199 user_group=None
200 )
200 )
201
201
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
202 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
203 member_reviewer['review_status'] = current_review_status
203 member_reviewer['review_status'] = current_review_status
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
204 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
205 member_reviewer['allowed_to_update'] = False
205 member_reviewer['allowed_to_update'] = False
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
206 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
207
207
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
208 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
209
209
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
210 # NOTE(marcink): this uses the same voting logic as in pull-requests
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
213
213
214 diff = None
214 diff = None
215 # Iterate over ranges (default commit view is always one commit)
215 # Iterate over ranges (default commit view is always one commit)
216 for commit in c.commit_ranges:
216 for commit in c.commit_ranges:
217 c.changes[commit.raw_id] = []
217 c.changes[commit.raw_id] = []
218
218
219 commit2 = commit
219 commit2 = commit
220 commit1 = commit.first_parent
220 commit1 = commit.first_parent
221
221
222 if method == 'show':
222 if method == 'show':
223 inline_comments = CommentsModel().get_inline_comments(
223 inline_comments = CommentsModel().get_inline_comments(
224 self.db_repo.repo_id, revision=commit.raw_id)
224 self.db_repo.repo_id, revision=commit.raw_id)
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
225 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
226 inline_comments))
226 inline_comments))
227 c.inline_comments = inline_comments
227 c.inline_comments = inline_comments
228
228
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
229 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
230 self.db_repo)
230 self.db_repo)
231 cache_file_path = diff_cache_exist(
231 cache_file_path = diff_cache_exist(
232 cache_path, 'diff', commit.raw_id,
232 cache_path, 'diff', commit.raw_id,
233 hide_whitespace_changes, diff_context, c.fulldiff)
233 hide_whitespace_changes, diff_context, c.fulldiff)
234
234
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
235 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
236 force_recache = str2bool(self.request.GET.get('force_recache'))
236 force_recache = str2bool(self.request.GET.get('force_recache'))
237
237
238 cached_diff = None
238 cached_diff = None
239 if caching_enabled:
239 if caching_enabled:
240 cached_diff = load_cached_diff(cache_file_path)
240 cached_diff = load_cached_diff(cache_file_path)
241
241
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
242 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
243 if not force_recache and has_proper_diff_cache:
243 if not force_recache and has_proper_diff_cache:
244 diffset = cached_diff['diff']
244 diffset = cached_diff['diff']
245 else:
245 else:
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
246 vcs_diff = self.rhodecode_vcs_repo.get_diff(
247 commit1, commit2,
247 commit1, commit2,
248 ignore_whitespace=hide_whitespace_changes,
248 ignore_whitespace=hide_whitespace_changes,
249 context=diff_context)
249 context=diff_context)
250
250
251 diff_processor = diffs.DiffProcessor(
251 diff_processor = diffs.DiffProcessor(
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
252 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 file_limit=file_limit, show_full_diff=c.fulldiff)
253 file_limit=file_limit, show_full_diff=c.fulldiff)
254
254
255 _parsed = diff_processor.prepare()
255 _parsed = diff_processor.prepare()
256
256
257 diffset = codeblocks.DiffSet(
257 diffset = codeblocks.DiffSet(
258 repo_name=self.db_repo_name,
258 repo_name=self.db_repo_name,
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
259 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
260 target_node_getter=codeblocks.diffset_node_getter(commit2))
261
261
262 diffset = self.path_filter.render_patchset_filtered(
262 diffset = self.path_filter.render_patchset_filtered(
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
263 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264
264
265 # save cached diff
265 # save cached diff
266 if caching_enabled:
266 if caching_enabled:
267 cache_diff(cache_file_path, diffset, None)
267 cache_diff(cache_file_path, diffset, None)
268
268
269 c.limited_diff = diffset.limited_diff
269 c.limited_diff = diffset.limited_diff
270 c.changes[commit.raw_id] = diffset
270 c.changes[commit.raw_id] = diffset
271 else:
271 else:
272 # TODO(marcink): no cache usage here...
272 # TODO(marcink): no cache usage here...
273 _diff = self.rhodecode_vcs_repo.get_diff(
273 _diff = self.rhodecode_vcs_repo.get_diff(
274 commit1, commit2,
274 commit1, commit2,
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
275 ignore_whitespace=hide_whitespace_changes, context=diff_context)
276 diff_processor = diffs.DiffProcessor(
276 diff_processor = diffs.DiffProcessor(
277 _diff, format='newdiff', diff_limit=diff_limit,
277 _diff, format='newdiff', diff_limit=diff_limit,
278 file_limit=file_limit, show_full_diff=c.fulldiff)
278 file_limit=file_limit, show_full_diff=c.fulldiff)
279 # downloads/raw we only need RAW diff nothing else
279 # downloads/raw we only need RAW diff nothing else
280 diff = self.path_filter.get_raw_patch(diff_processor)
280 diff = self.path_filter.get_raw_patch(diff_processor)
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
281 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
282
282
283 # sort comments by how they were generated
283 # sort comments by how they were generated
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
284 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
285 c.at_version_num = None
285 c.at_version_num = None
286
286
287 if len(c.commit_ranges) == 1:
287 if len(c.commit_ranges) == 1:
288 c.commit = c.commit_ranges[0]
288 c.commit = c.commit_ranges[0]
289 c.parent_tmpl = ''.join(
289 c.parent_tmpl = ''.join(
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
290 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
291
291
292 if method == 'download':
292 if method == 'download':
293 response = Response(diff)
293 response = Response(diff)
294 response.content_type = 'text/plain'
294 response.content_type = 'text/plain'
295 response.content_disposition = (
295 response.content_disposition = (
296 'attachment; filename=%s.diff' % commit_id_range[:12])
296 'attachment; filename=%s.diff' % commit_id_range[:12])
297 return response
297 return response
298 elif method == 'patch':
298 elif method == 'patch':
299 c.diff = safe_unicode(diff)
299 c.diff = safe_unicode(diff)
300 patch = render(
300 patch = render(
301 'rhodecode:templates/changeset/patch_changeset.mako',
301 'rhodecode:templates/changeset/patch_changeset.mako',
302 self._get_template_context(c), self.request)
302 self._get_template_context(c), self.request)
303 response = Response(patch)
303 response = Response(patch)
304 response.content_type = 'text/plain'
304 response.content_type = 'text/plain'
305 return response
305 return response
306 elif method == 'raw':
306 elif method == 'raw':
307 response = Response(diff)
307 response = Response(diff)
308 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
309 return response
309 return response
310 elif method == 'show':
310 elif method == 'show':
311 if len(c.commit_ranges) == 1:
311 if len(c.commit_ranges) == 1:
312 html = render(
312 html = render(
313 'rhodecode:templates/changeset/changeset.mako',
313 'rhodecode:templates/changeset/changeset.mako',
314 self._get_template_context(c), self.request)
314 self._get_template_context(c), self.request)
315 return Response(html)
315 return Response(html)
316 else:
316 else:
317 c.ancestor = None
317 c.ancestor = None
318 c.target_repo = self.db_repo
318 c.target_repo = self.db_repo
319 html = render(
319 html = render(
320 'rhodecode:templates/changeset/changeset_range.mako',
320 'rhodecode:templates/changeset/changeset_range.mako',
321 self._get_template_context(c), self.request)
321 self._get_template_context(c), self.request)
322 return Response(html)
322 return Response(html)
323
323
324 raise HTTPBadRequest()
324 raise HTTPBadRequest()
325
325
326 @LoginRequired()
326 @LoginRequired()
327 @HasRepoPermissionAnyDecorator(
327 @HasRepoPermissionAnyDecorator(
328 'repository.read', 'repository.write', 'repository.admin')
328 'repository.read', 'repository.write', 'repository.admin')
329 @view_config(
329 @view_config(
330 route_name='repo_commit', request_method='GET',
330 route_name='repo_commit', request_method='GET',
331 renderer=None)
331 renderer=None)
332 def repo_commit_show(self):
332 def repo_commit_show(self):
333 commit_id = self.request.matchdict['commit_id']
333 commit_id = self.request.matchdict['commit_id']
334 return self._commit(commit_id, method='show')
334 return self._commit(commit_id, method='show')
335
335
336 @LoginRequired()
336 @LoginRequired()
337 @HasRepoPermissionAnyDecorator(
337 @HasRepoPermissionAnyDecorator(
338 'repository.read', 'repository.write', 'repository.admin')
338 'repository.read', 'repository.write', 'repository.admin')
339 @view_config(
339 @view_config(
340 route_name='repo_commit_raw', request_method='GET',
340 route_name='repo_commit_raw', request_method='GET',
341 renderer=None)
341 renderer=None)
342 @view_config(
342 @view_config(
343 route_name='repo_commit_raw_deprecated', request_method='GET',
343 route_name='repo_commit_raw_deprecated', request_method='GET',
344 renderer=None)
344 renderer=None)
345 def repo_commit_raw(self):
345 def repo_commit_raw(self):
346 commit_id = self.request.matchdict['commit_id']
346 commit_id = self.request.matchdict['commit_id']
347 return self._commit(commit_id, method='raw')
347 return self._commit(commit_id, method='raw')
348
348
349 @LoginRequired()
349 @LoginRequired()
350 @HasRepoPermissionAnyDecorator(
350 @HasRepoPermissionAnyDecorator(
351 'repository.read', 'repository.write', 'repository.admin')
351 'repository.read', 'repository.write', 'repository.admin')
352 @view_config(
352 @view_config(
353 route_name='repo_commit_patch', request_method='GET',
353 route_name='repo_commit_patch', request_method='GET',
354 renderer=None)
354 renderer=None)
355 def repo_commit_patch(self):
355 def repo_commit_patch(self):
356 commit_id = self.request.matchdict['commit_id']
356 commit_id = self.request.matchdict['commit_id']
357 return self._commit(commit_id, method='patch')
357 return self._commit(commit_id, method='patch')
358
358
359 @LoginRequired()
359 @LoginRequired()
360 @HasRepoPermissionAnyDecorator(
360 @HasRepoPermissionAnyDecorator(
361 'repository.read', 'repository.write', 'repository.admin')
361 'repository.read', 'repository.write', 'repository.admin')
362 @view_config(
362 @view_config(
363 route_name='repo_commit_download', request_method='GET',
363 route_name='repo_commit_download', request_method='GET',
364 renderer=None)
364 renderer=None)
365 def repo_commit_download(self):
365 def repo_commit_download(self):
366 commit_id = self.request.matchdict['commit_id']
366 commit_id = self.request.matchdict['commit_id']
367 return self._commit(commit_id, method='download')
367 return self._commit(commit_id, method='download')
368
368
369 @LoginRequired()
369 @LoginRequired()
370 @NotAnonymous()
370 @NotAnonymous()
371 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
373 @CSRFRequired()
373 @CSRFRequired()
374 @view_config(
374 @view_config(
375 route_name='repo_commit_comment_create', request_method='POST',
375 route_name='repo_commit_comment_create', request_method='POST',
376 renderer='json_ext')
376 renderer='json_ext')
377 def repo_commit_comment_create(self):
377 def repo_commit_comment_create(self):
378 _ = self.request.translate
378 _ = self.request.translate
379 commit_id = self.request.matchdict['commit_id']
379 commit_id = self.request.matchdict['commit_id']
380
380
381 c = self.load_default_context()
381 c = self.load_default_context()
382 status = self.request.POST.get('changeset_status', None)
382 status = self.request.POST.get('changeset_status', None)
383 text = self.request.POST.get('text')
383 text = self.request.POST.get('text')
384 comment_type = self.request.POST.get('comment_type')
384 comment_type = self.request.POST.get('comment_type')
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
385 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
386 f_path = self.request.POST.get('f_path')
387 line_no = self.request.POST.get('line')
388 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
386
389
387 if status:
390 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
391 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
392 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
393 'status': ChangesetStatus.get_status_lbl(status)})
391
394
392 multi_commit_ids = []
395 multi_commit_ids = []
393 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
396 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
394 if _commit_id not in ['', None, EmptyCommit.raw_id]:
397 if _commit_id not in ['', None, EmptyCommit.raw_id]:
395 if _commit_id not in multi_commit_ids:
398 if _commit_id not in multi_commit_ids:
396 multi_commit_ids.append(_commit_id)
399 multi_commit_ids.append(_commit_id)
397
400
398 commit_ids = multi_commit_ids or [commit_id]
401 commit_ids = multi_commit_ids or [commit_id]
399
402
400 comment = None
403 comment = None
401 for current_id in filter(None, commit_ids):
404 for current_id in filter(None, commit_ids):
402 comment = CommentsModel().create(
405 comment = CommentsModel().create(
403 text=text,
406 text=text,
404 repo=self.db_repo.repo_id,
407 repo=self.db_repo.repo_id,
405 user=self._rhodecode_db_user.user_id,
408 user=self._rhodecode_db_user.user_id,
406 commit_id=current_id,
409 commit_id=current_id,
407 f_path=self.request.POST.get('f_path'),
410 f_path=f_path,
408 line_no=self.request.POST.get('line'),
411 line_no=line_no,
409 status_change=(ChangesetStatus.get_status_lbl(status)
412 status_change=(ChangesetStatus.get_status_lbl(status)
410 if status else None),
413 if status else None),
411 status_change_type=status,
414 status_change_type=status,
412 comment_type=comment_type,
415 comment_type=comment_type,
413 resolves_comment_id=resolves_comment_id,
416 resolves_comment_id=resolves_comment_id,
414 auth_user=self._rhodecode_user
417 auth_user=self._rhodecode_user
415 )
418 )
416 is_inline = comment.is_inline
419 is_inline = comment.is_inline
417
420
418 # get status if set !
421 # get status if set !
419 if status:
422 if status:
420 # if latest status was from pull request and it's closed
423 # if latest status was from pull request and it's closed
421 # disallow changing status !
424 # disallow changing status !
422 # dont_allow_on_closed_pull_request = True !
425 # dont_allow_on_closed_pull_request = True !
423
426
424 try:
427 try:
425 ChangesetStatusModel().set_status(
428 ChangesetStatusModel().set_status(
426 self.db_repo.repo_id,
429 self.db_repo.repo_id,
427 status,
430 status,
428 self._rhodecode_db_user.user_id,
431 self._rhodecode_db_user.user_id,
429 comment,
432 comment,
430 revision=current_id,
433 revision=current_id,
431 dont_allow_on_closed_pull_request=True
434 dont_allow_on_closed_pull_request=True
432 )
435 )
433 except StatusChangeOnClosedPullRequestError:
436 except StatusChangeOnClosedPullRequestError:
434 msg = _('Changing the status of a commit associated with '
437 msg = _('Changing the status of a commit associated with '
435 'a closed pull request is not allowed')
438 'a closed pull request is not allowed')
436 log.exception(msg)
439 log.exception(msg)
437 h.flash(msg, category='warning')
440 h.flash(msg, category='warning')
438 raise HTTPFound(h.route_path(
441 raise HTTPFound(h.route_path(
439 'repo_commit', repo_name=self.db_repo_name,
442 'repo_commit', repo_name=self.db_repo_name,
440 commit_id=current_id))
443 commit_id=current_id))
441
444
442 commit = self.db_repo.get_commit(current_id)
445 commit = self.db_repo.get_commit(current_id)
443 CommentsModel().trigger_commit_comment_hook(
446 CommentsModel().trigger_commit_comment_hook(
444 self.db_repo, self._rhodecode_user, 'create',
447 self.db_repo, self._rhodecode_user, 'create',
445 data={'comment': comment, 'commit': commit})
448 data={'comment': comment, 'commit': commit})
446
449
447 # finalize, commit and redirect
450 # finalize, commit and redirect
448 Session().commit()
451 Session().commit()
449
452
450 data = {
453 data = {}
451 'target_id': h.safeid(h.safe_unicode(
454 if comment:
452 self.request.POST.get('f_path'))),
455 comment_id = comment.comment_id
456 data[comment_id] = {
457 'target_id': target_elem_id
453 }
458 }
454 if comment:
455 c.co = comment
459 c.co = comment
456 c.at_version_num = 0
460 c.at_version_num = 0
457 rendered_comment = render(
461 rendered_comment = render(
458 'rhodecode:templates/changeset/changeset_comment_block.mako',
462 'rhodecode:templates/changeset/changeset_comment_block.mako',
459 self._get_template_context(c), self.request)
463 self._get_template_context(c), self.request)
460
464
461 data.update(comment.get_dict())
465 data[comment_id].update(comment.get_dict())
462 data.update({'rendered_text': rendered_comment})
466 data[comment_id].update({'rendered_text': rendered_comment})
463
467
464 comment_broadcast_channel = channelstream.comment_channel(
468 comment_broadcast_channel = channelstream.comment_channel(
465 self.db_repo_name, commit_obj=commit)
469 self.db_repo_name, commit_obj=commit)
466
470
467 comment_data = data
471 comment_data = data
468 comment_type = 'inline' if is_inline else 'general'
472 comment_type = 'inline' if is_inline else 'general'
469 channelstream.comment_channelstream_push(
473 channelstream.comment_channelstream_push(
470 self.request, comment_broadcast_channel, self._rhodecode_user,
474 self.request, comment_broadcast_channel, self._rhodecode_user,
471 _('posted a new {} comment').format(comment_type),
475 _('posted a new {} comment').format(comment_type),
472 comment_data=comment_data)
476 comment_data=comment_data)
473
477
474 return data
478 return data
475
479
476 @LoginRequired()
480 @LoginRequired()
477 @NotAnonymous()
481 @NotAnonymous()
478 @HasRepoPermissionAnyDecorator(
482 @HasRepoPermissionAnyDecorator(
479 'repository.read', 'repository.write', 'repository.admin')
483 'repository.read', 'repository.write', 'repository.admin')
480 @CSRFRequired()
484 @CSRFRequired()
481 @view_config(
485 @view_config(
482 route_name='repo_commit_comment_preview', request_method='POST',
486 route_name='repo_commit_comment_preview', request_method='POST',
483 renderer='string', xhr=True)
487 renderer='string', xhr=True)
484 def repo_commit_comment_preview(self):
488 def repo_commit_comment_preview(self):
485 # Technically a CSRF token is not needed as no state changes with this
489 # Technically a CSRF token is not needed as no state changes with this
486 # call. However, as this is a POST is better to have it, so automated
490 # call. However, as this is a POST is better to have it, so automated
487 # tools don't flag it as potential CSRF.
491 # tools don't flag it as potential CSRF.
488 # Post is required because the payload could be bigger than the maximum
492 # Post is required because the payload could be bigger than the maximum
489 # allowed by GET.
493 # allowed by GET.
490
494
491 text = self.request.POST.get('text')
495 text = self.request.POST.get('text')
492 renderer = self.request.POST.get('renderer') or 'rst'
496 renderer = self.request.POST.get('renderer') or 'rst'
493 if text:
497 if text:
494 return h.render(text, renderer=renderer, mentions=True,
498 return h.render(text, renderer=renderer, mentions=True,
495 repo_name=self.db_repo_name)
499 repo_name=self.db_repo_name)
496 return ''
500 return ''
497
501
498 @LoginRequired()
502 @LoginRequired()
499 @HasRepoPermissionAnyDecorator(
503 @HasRepoPermissionAnyDecorator(
500 'repository.read', 'repository.write', 'repository.admin')
504 'repository.read', 'repository.write', 'repository.admin')
501 @CSRFRequired()
505 @CSRFRequired()
502 @view_config(
506 @view_config(
503 route_name='repo_commit_comment_history_view', request_method='POST',
507 route_name='repo_commit_comment_history_view', request_method='POST',
504 renderer='string', xhr=True)
508 renderer='string', xhr=True)
505 def repo_commit_comment_history_view(self):
509 def repo_commit_comment_history_view(self):
506 c = self.load_default_context()
510 c = self.load_default_context()
507
511
508 comment_history_id = self.request.matchdict['comment_history_id']
512 comment_history_id = self.request.matchdict['comment_history_id']
509 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
513 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
510 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
514 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
511
515
512 if is_repo_comment:
516 if is_repo_comment:
513 c.comment_history = comment_history
517 c.comment_history = comment_history
514
518
515 rendered_comment = render(
519 rendered_comment = render(
516 'rhodecode:templates/changeset/comment_history.mako',
520 'rhodecode:templates/changeset/comment_history.mako',
517 self._get_template_context(c)
521 self._get_template_context(c)
518 , self.request)
522 , self.request)
519 return rendered_comment
523 return rendered_comment
520 else:
524 else:
521 log.warning('No permissions for user %s to show comment_history_id: %s',
525 log.warning('No permissions for user %s to show comment_history_id: %s',
522 self._rhodecode_db_user, comment_history_id)
526 self._rhodecode_db_user, comment_history_id)
523 raise HTTPNotFound()
527 raise HTTPNotFound()
524
528
525 @LoginRequired()
529 @LoginRequired()
526 @NotAnonymous()
530 @NotAnonymous()
527 @HasRepoPermissionAnyDecorator(
531 @HasRepoPermissionAnyDecorator(
528 'repository.read', 'repository.write', 'repository.admin')
532 'repository.read', 'repository.write', 'repository.admin')
529 @CSRFRequired()
533 @CSRFRequired()
530 @view_config(
534 @view_config(
531 route_name='repo_commit_comment_attachment_upload', request_method='POST',
535 route_name='repo_commit_comment_attachment_upload', request_method='POST',
532 renderer='json_ext', xhr=True)
536 renderer='json_ext', xhr=True)
533 def repo_commit_comment_attachment_upload(self):
537 def repo_commit_comment_attachment_upload(self):
534 c = self.load_default_context()
538 c = self.load_default_context()
535 upload_key = 'attachment'
539 upload_key = 'attachment'
536
540
537 file_obj = self.request.POST.get(upload_key)
541 file_obj = self.request.POST.get(upload_key)
538
542
539 if file_obj is None:
543 if file_obj is None:
540 self.request.response.status = 400
544 self.request.response.status = 400
541 return {'store_fid': None,
545 return {'store_fid': None,
542 'access_path': None,
546 'access_path': None,
543 'error': '{} data field is missing'.format(upload_key)}
547 'error': '{} data field is missing'.format(upload_key)}
544
548
545 if not hasattr(file_obj, 'filename'):
549 if not hasattr(file_obj, 'filename'):
546 self.request.response.status = 400
550 self.request.response.status = 400
547 return {'store_fid': None,
551 return {'store_fid': None,
548 'access_path': None,
552 'access_path': None,
549 'error': 'filename cannot be read from the data field'}
553 'error': 'filename cannot be read from the data field'}
550
554
551 filename = file_obj.filename
555 filename = file_obj.filename
552 file_display_name = filename
556 file_display_name = filename
553
557
554 metadata = {
558 metadata = {
555 'user_uploaded': {'username': self._rhodecode_user.username,
559 'user_uploaded': {'username': self._rhodecode_user.username,
556 'user_id': self._rhodecode_user.user_id,
560 'user_id': self._rhodecode_user.user_id,
557 'ip': self._rhodecode_user.ip_addr}}
561 'ip': self._rhodecode_user.ip_addr}}
558
562
559 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
563 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
560 allowed_extensions = [
564 allowed_extensions = [
561 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
565 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
562 '.pptx', '.txt', '.xlsx', '.zip']
566 '.pptx', '.txt', '.xlsx', '.zip']
563 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
567 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
564
568
565 try:
569 try:
566 storage = store_utils.get_file_storage(self.request.registry.settings)
570 storage = store_utils.get_file_storage(self.request.registry.settings)
567 store_uid, metadata = storage.save_file(
571 store_uid, metadata = storage.save_file(
568 file_obj.file, filename, extra_metadata=metadata,
572 file_obj.file, filename, extra_metadata=metadata,
569 extensions=allowed_extensions, max_filesize=max_file_size)
573 extensions=allowed_extensions, max_filesize=max_file_size)
570 except FileNotAllowedException:
574 except FileNotAllowedException:
571 self.request.response.status = 400
575 self.request.response.status = 400
572 permitted_extensions = ', '.join(allowed_extensions)
576 permitted_extensions = ', '.join(allowed_extensions)
573 error_msg = 'File `{}` is not allowed. ' \
577 error_msg = 'File `{}` is not allowed. ' \
574 'Only following extensions are permitted: {}'.format(
578 'Only following extensions are permitted: {}'.format(
575 filename, permitted_extensions)
579 filename, permitted_extensions)
576 return {'store_fid': None,
580 return {'store_fid': None,
577 'access_path': None,
581 'access_path': None,
578 'error': error_msg}
582 'error': error_msg}
579 except FileOverSizeException:
583 except FileOverSizeException:
580 self.request.response.status = 400
584 self.request.response.status = 400
581 limit_mb = h.format_byte_size_binary(max_file_size)
585 limit_mb = h.format_byte_size_binary(max_file_size)
582 return {'store_fid': None,
586 return {'store_fid': None,
583 'access_path': None,
587 'access_path': None,
584 'error': 'File {} is exceeding allowed limit of {}.'.format(
588 'error': 'File {} is exceeding allowed limit of {}.'.format(
585 filename, limit_mb)}
589 filename, limit_mb)}
586
590
587 try:
591 try:
588 entry = FileStore.create(
592 entry = FileStore.create(
589 file_uid=store_uid, filename=metadata["filename"],
593 file_uid=store_uid, filename=metadata["filename"],
590 file_hash=metadata["sha256"], file_size=metadata["size"],
594 file_hash=metadata["sha256"], file_size=metadata["size"],
591 file_display_name=file_display_name,
595 file_display_name=file_display_name,
592 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
596 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
593 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
597 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
594 scope_repo_id=self.db_repo.repo_id
598 scope_repo_id=self.db_repo.repo_id
595 )
599 )
596 Session().add(entry)
600 Session().add(entry)
597 Session().commit()
601 Session().commit()
598 log.debug('Stored upload in DB as %s', entry)
602 log.debug('Stored upload in DB as %s', entry)
599 except Exception:
603 except Exception:
600 log.exception('Failed to store file %s', filename)
604 log.exception('Failed to store file %s', filename)
601 self.request.response.status = 400
605 self.request.response.status = 400
602 return {'store_fid': None,
606 return {'store_fid': None,
603 'access_path': None,
607 'access_path': None,
604 'error': 'File {} failed to store in DB.'.format(filename)}
608 'error': 'File {} failed to store in DB.'.format(filename)}
605
609
606 Session().commit()
610 Session().commit()
607
611
608 return {
612 return {
609 'store_fid': store_uid,
613 'store_fid': store_uid,
610 'access_path': h.route_path(
614 'access_path': h.route_path(
611 'download_file', fid=store_uid),
615 'download_file', fid=store_uid),
612 'fqn_access_path': h.route_url(
616 'fqn_access_path': h.route_url(
613 'download_file', fid=store_uid),
617 'download_file', fid=store_uid),
614 'repo_access_path': h.route_path(
618 'repo_access_path': h.route_path(
615 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
619 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
616 'repo_fqn_access_path': h.route_url(
620 'repo_fqn_access_path': h.route_url(
617 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
621 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
618 }
622 }
619
623
620 @LoginRequired()
624 @LoginRequired()
621 @NotAnonymous()
625 @NotAnonymous()
622 @HasRepoPermissionAnyDecorator(
626 @HasRepoPermissionAnyDecorator(
623 'repository.read', 'repository.write', 'repository.admin')
627 'repository.read', 'repository.write', 'repository.admin')
624 @CSRFRequired()
628 @CSRFRequired()
625 @view_config(
629 @view_config(
626 route_name='repo_commit_comment_delete', request_method='POST',
630 route_name='repo_commit_comment_delete', request_method='POST',
627 renderer='json_ext')
631 renderer='json_ext')
628 def repo_commit_comment_delete(self):
632 def repo_commit_comment_delete(self):
629 commit_id = self.request.matchdict['commit_id']
633 commit_id = self.request.matchdict['commit_id']
630 comment_id = self.request.matchdict['comment_id']
634 comment_id = self.request.matchdict['comment_id']
631
635
632 comment = ChangesetComment.get_or_404(comment_id)
636 comment = ChangesetComment.get_or_404(comment_id)
633 if not comment:
637 if not comment:
634 log.debug('Comment with id:%s not found, skipping', comment_id)
638 log.debug('Comment with id:%s not found, skipping', comment_id)
635 # comment already deleted in another call probably
639 # comment already deleted in another call probably
636 return True
640 return True
637
641
638 if comment.immutable:
642 if comment.immutable:
639 # don't allow deleting comments that are immutable
643 # don't allow deleting comments that are immutable
640 raise HTTPForbidden()
644 raise HTTPForbidden()
641
645
642 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
646 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
643 super_admin = h.HasPermissionAny('hg.admin')()
647 super_admin = h.HasPermissionAny('hg.admin')()
644 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
648 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
645 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
649 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
646 comment_repo_admin = is_repo_admin and is_repo_comment
650 comment_repo_admin = is_repo_admin and is_repo_comment
647
651
648 if super_admin or comment_owner or comment_repo_admin:
652 if super_admin or comment_owner or comment_repo_admin:
649 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
653 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
650 Session().commit()
654 Session().commit()
651 return True
655 return True
652 else:
656 else:
653 log.warning('No permissions for user %s to delete comment_id: %s',
657 log.warning('No permissions for user %s to delete comment_id: %s',
654 self._rhodecode_db_user, comment_id)
658 self._rhodecode_db_user, comment_id)
655 raise HTTPNotFound()
659 raise HTTPNotFound()
656
660
657 @LoginRequired()
661 @LoginRequired()
658 @NotAnonymous()
662 @NotAnonymous()
659 @HasRepoPermissionAnyDecorator(
663 @HasRepoPermissionAnyDecorator(
660 'repository.read', 'repository.write', 'repository.admin')
664 'repository.read', 'repository.write', 'repository.admin')
661 @CSRFRequired()
665 @CSRFRequired()
662 @view_config(
666 @view_config(
663 route_name='repo_commit_comment_edit', request_method='POST',
667 route_name='repo_commit_comment_edit', request_method='POST',
664 renderer='json_ext')
668 renderer='json_ext')
665 def repo_commit_comment_edit(self):
669 def repo_commit_comment_edit(self):
666 self.load_default_context()
670 self.load_default_context()
667
671
668 comment_id = self.request.matchdict['comment_id']
672 comment_id = self.request.matchdict['comment_id']
669 comment = ChangesetComment.get_or_404(comment_id)
673 comment = ChangesetComment.get_or_404(comment_id)
670
674
671 if comment.immutable:
675 if comment.immutable:
672 # don't allow deleting comments that are immutable
676 # don't allow deleting comments that are immutable
673 raise HTTPForbidden()
677 raise HTTPForbidden()
674
678
675 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
679 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
676 super_admin = h.HasPermissionAny('hg.admin')()
680 super_admin = h.HasPermissionAny('hg.admin')()
677 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
681 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
678 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
682 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
679 comment_repo_admin = is_repo_admin and is_repo_comment
683 comment_repo_admin = is_repo_admin and is_repo_comment
680
684
681 if super_admin or comment_owner or comment_repo_admin:
685 if super_admin or comment_owner or comment_repo_admin:
682 text = self.request.POST.get('text')
686 text = self.request.POST.get('text')
683 version = self.request.POST.get('version')
687 version = self.request.POST.get('version')
684 if text == comment.text:
688 if text == comment.text:
685 log.warning(
689 log.warning(
686 'Comment(repo): '
690 'Comment(repo): '
687 'Trying to create new version '
691 'Trying to create new version '
688 'with the same comment body {}'.format(
692 'with the same comment body {}'.format(
689 comment_id,
693 comment_id,
690 )
694 )
691 )
695 )
692 raise HTTPNotFound()
696 raise HTTPNotFound()
693
697
694 if version.isdigit():
698 if version.isdigit():
695 version = int(version)
699 version = int(version)
696 else:
700 else:
697 log.warning(
701 log.warning(
698 'Comment(repo): Wrong version type {} {} '
702 'Comment(repo): Wrong version type {} {} '
699 'for comment {}'.format(
703 'for comment {}'.format(
700 version,
704 version,
701 type(version),
705 type(version),
702 comment_id,
706 comment_id,
703 )
707 )
704 )
708 )
705 raise HTTPNotFound()
709 raise HTTPNotFound()
706
710
707 try:
711 try:
708 comment_history = CommentsModel().edit(
712 comment_history = CommentsModel().edit(
709 comment_id=comment_id,
713 comment_id=comment_id,
710 text=text,
714 text=text,
711 auth_user=self._rhodecode_user,
715 auth_user=self._rhodecode_user,
712 version=version,
716 version=version,
713 )
717 )
714 except CommentVersionMismatch:
718 except CommentVersionMismatch:
715 raise HTTPConflict()
719 raise HTTPConflict()
716
720
717 if not comment_history:
721 if not comment_history:
718 raise HTTPNotFound()
722 raise HTTPNotFound()
719
723
720 commit_id = self.request.matchdict['commit_id']
724 commit_id = self.request.matchdict['commit_id']
721 commit = self.db_repo.get_commit(commit_id)
725 commit = self.db_repo.get_commit(commit_id)
722 CommentsModel().trigger_commit_comment_hook(
726 CommentsModel().trigger_commit_comment_hook(
723 self.db_repo, self._rhodecode_user, 'edit',
727 self.db_repo, self._rhodecode_user, 'edit',
724 data={'comment': comment, 'commit': commit})
728 data={'comment': comment, 'commit': commit})
725
729
726 Session().commit()
730 Session().commit()
727 return {
731 return {
728 'comment_history_id': comment_history.comment_history_id,
732 'comment_history_id': comment_history.comment_history_id,
729 'comment_id': comment.comment_id,
733 'comment_id': comment.comment_id,
730 'comment_version': comment_history.version,
734 'comment_version': comment_history.version,
731 'comment_author_username': comment_history.author.username,
735 'comment_author_username': comment_history.author.username,
732 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
736 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
733 'comment_created_on': h.age_component(comment_history.created_on,
737 'comment_created_on': h.age_component(comment_history.created_on,
734 time_is_local=True),
738 time_is_local=True),
735 }
739 }
736 else:
740 else:
737 log.warning('No permissions for user %s to edit comment_id: %s',
741 log.warning('No permissions for user %s to edit comment_id: %s',
738 self._rhodecode_db_user, comment_id)
742 self._rhodecode_db_user, comment_id)
739 raise HTTPNotFound()
743 raise HTTPNotFound()
740
744
741 @LoginRequired()
745 @LoginRequired()
742 @HasRepoPermissionAnyDecorator(
746 @HasRepoPermissionAnyDecorator(
743 'repository.read', 'repository.write', 'repository.admin')
747 'repository.read', 'repository.write', 'repository.admin')
744 @view_config(
748 @view_config(
745 route_name='repo_commit_data', request_method='GET',
749 route_name='repo_commit_data', request_method='GET',
746 renderer='json_ext', xhr=True)
750 renderer='json_ext', xhr=True)
747 def repo_commit_data(self):
751 def repo_commit_data(self):
748 commit_id = self.request.matchdict['commit_id']
752 commit_id = self.request.matchdict['commit_id']
749 self.load_default_context()
753 self.load_default_context()
750
754
751 try:
755 try:
752 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
756 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
753 except CommitDoesNotExistError as e:
757 except CommitDoesNotExistError as e:
754 return EmptyCommit(message=str(e))
758 return EmptyCommit(message=str(e))
755
759
756 @LoginRequired()
760 @LoginRequired()
757 @HasRepoPermissionAnyDecorator(
761 @HasRepoPermissionAnyDecorator(
758 'repository.read', 'repository.write', 'repository.admin')
762 'repository.read', 'repository.write', 'repository.admin')
759 @view_config(
763 @view_config(
760 route_name='repo_commit_children', request_method='GET',
764 route_name='repo_commit_children', request_method='GET',
761 renderer='json_ext', xhr=True)
765 renderer='json_ext', xhr=True)
762 def repo_commit_children(self):
766 def repo_commit_children(self):
763 commit_id = self.request.matchdict['commit_id']
767 commit_id = self.request.matchdict['commit_id']
764 self.load_default_context()
768 self.load_default_context()
765
769
766 try:
770 try:
767 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
771 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
768 children = commit.children
772 children = commit.children
769 except CommitDoesNotExistError:
773 except CommitDoesNotExistError:
770 children = []
774 children = []
771
775
772 result = {"results": children}
776 result = {"results": children}
773 return result
777 return result
774
778
775 @LoginRequired()
779 @LoginRequired()
776 @HasRepoPermissionAnyDecorator(
780 @HasRepoPermissionAnyDecorator(
777 'repository.read', 'repository.write', 'repository.admin')
781 'repository.read', 'repository.write', 'repository.admin')
778 @view_config(
782 @view_config(
779 route_name='repo_commit_parents', request_method='GET',
783 route_name='repo_commit_parents', request_method='GET',
780 renderer='json_ext')
784 renderer='json_ext')
781 def repo_commit_parents(self):
785 def repo_commit_parents(self):
782 commit_id = self.request.matchdict['commit_id']
786 commit_id = self.request.matchdict['commit_id']
783 self.load_default_context()
787 self.load_default_context()
784
788
785 try:
789 try:
786 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
790 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 parents = commit.parents
791 parents = commit.parents
788 except CommitDoesNotExistError:
792 except CommitDoesNotExistError:
789 parents = []
793 parents = []
790 result = {"results": parents}
794 result = {"results": parents}
791 return result
795 return result
@@ -1,1826 +1,1856 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
87 opened_by=opened_by)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
97 else:
97 else:
98 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
104 opened_by=opened_by)
105
105
106 data = []
106 data = []
107 comments_model = CommentsModel()
107 comments_model = CommentsModel()
108 for pr in pull_requests:
108 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
111
111
112 data.append({
112 data.append({
113 'name': _render('pullrequest_name',
113 'name': _render('pullrequest_name',
114 pr.pull_request_id, pr.pull_request_state,
114 pr.pull_request_id, pr.pull_request_state,
115 pr.work_in_progress, pr.target_repo.repo_name,
115 pr.work_in_progress, pr.target_repo.repo_name,
116 short=True),
116 short=True),
117 'name_raw': pr.pull_request_id,
117 'name_raw': pr.pull_request_id,
118 'status': _render('pullrequest_status',
118 'status': _render('pullrequest_status',
119 pr.calculated_review_status()),
119 pr.calculated_review_status()),
120 'title': _render('pullrequest_title', pr.title, pr.description),
120 'title': _render('pullrequest_title', pr.title, pr.description),
121 'description': h.escape(pr.description),
121 'description': h.escape(pr.description),
122 'updated_on': _render('pullrequest_updated_on',
122 'updated_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.updated_on)),
123 h.datetime_to_time(pr.updated_on)),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
124 'updated_on_raw': h.datetime_to_time(pr.updated_on),
125 'created_on': _render('pullrequest_updated_on',
125 'created_on': _render('pullrequest_updated_on',
126 h.datetime_to_time(pr.created_on)),
126 h.datetime_to_time(pr.created_on)),
127 'created_on_raw': h.datetime_to_time(pr.created_on),
127 'created_on_raw': h.datetime_to_time(pr.created_on),
128 'state': pr.pull_request_state,
128 'state': pr.pull_request_state,
129 'author': _render('pullrequest_author',
129 'author': _render('pullrequest_author',
130 pr.author.full_contact, ),
130 pr.author.full_contact, ),
131 'author_raw': pr.author.full_name,
131 'author_raw': pr.author.full_name,
132 'comments': _render('pullrequest_comments', comments_count),
132 'comments': _render('pullrequest_comments', comments_count),
133 'comments_raw': comments_count,
133 'comments_raw': comments_count,
134 'closed': pr.is_closed(),
134 'closed': pr.is_closed(),
135 })
135 })
136
136
137 data = ({
137 data = ({
138 'draw': draw,
138 'draw': draw,
139 'data': data,
139 'data': data,
140 'recordsTotal': pull_requests_total_count,
140 'recordsTotal': pull_requests_total_count,
141 'recordsFiltered': pull_requests_total_count,
141 'recordsFiltered': pull_requests_total_count,
142 })
142 })
143 return data
143 return data
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @HasRepoPermissionAnyDecorator(
146 @HasRepoPermissionAnyDecorator(
147 'repository.read', 'repository.write', 'repository.admin')
147 'repository.read', 'repository.write', 'repository.admin')
148 @view_config(
148 @view_config(
149 route_name='pullrequest_show_all', request_method='GET',
149 route_name='pullrequest_show_all', request_method='GET',
150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
150 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
151 def pull_request_list(self):
151 def pull_request_list(self):
152 c = self.load_default_context()
152 c = self.load_default_context()
153
153
154 req_get = self.request.GET
154 req_get = self.request.GET
155 c.source = str2bool(req_get.get('source'))
155 c.source = str2bool(req_get.get('source'))
156 c.closed = str2bool(req_get.get('closed'))
156 c.closed = str2bool(req_get.get('closed'))
157 c.my = str2bool(req_get.get('my'))
157 c.my = str2bool(req_get.get('my'))
158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
160
160
161 c.active = 'open'
161 c.active = 'open'
162 if c.my:
162 if c.my:
163 c.active = 'my'
163 c.active = 'my'
164 if c.closed:
164 if c.closed:
165 c.active = 'closed'
165 c.active = 'closed'
166 if c.awaiting_review and not c.source:
166 if c.awaiting_review and not c.source:
167 c.active = 'awaiting'
167 c.active = 'awaiting'
168 if c.source and not c.awaiting_review:
168 if c.source and not c.awaiting_review:
169 c.active = 'source'
169 c.active = 'source'
170 if c.awaiting_my_review:
170 if c.awaiting_my_review:
171 c.active = 'awaiting_my'
171 c.active = 'awaiting_my'
172
172
173 return self._get_template_context(c)
173 return self._get_template_context(c)
174
174
175 @LoginRequired()
175 @LoginRequired()
176 @HasRepoPermissionAnyDecorator(
176 @HasRepoPermissionAnyDecorator(
177 'repository.read', 'repository.write', 'repository.admin')
177 'repository.read', 'repository.write', 'repository.admin')
178 @view_config(
178 @view_config(
179 route_name='pullrequest_show_all_data', request_method='GET',
179 route_name='pullrequest_show_all_data', request_method='GET',
180 renderer='json_ext', xhr=True)
180 renderer='json_ext', xhr=True)
181 def pull_request_list_data(self):
181 def pull_request_list_data(self):
182 self.load_default_context()
182 self.load_default_context()
183
183
184 # additional filters
184 # additional filters
185 req_get = self.request.GET
185 req_get = self.request.GET
186 source = str2bool(req_get.get('source'))
186 source = str2bool(req_get.get('source'))
187 closed = str2bool(req_get.get('closed'))
187 closed = str2bool(req_get.get('closed'))
188 my = str2bool(req_get.get('my'))
188 my = str2bool(req_get.get('my'))
189 awaiting_review = str2bool(req_get.get('awaiting_review'))
189 awaiting_review = str2bool(req_get.get('awaiting_review'))
190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
190 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
191
191
192 filter_type = 'awaiting_review' if awaiting_review \
192 filter_type = 'awaiting_review' if awaiting_review \
193 else 'awaiting_my_review' if awaiting_my_review \
193 else 'awaiting_my_review' if awaiting_my_review \
194 else None
194 else None
195
195
196 opened_by = None
196 opened_by = None
197 if my:
197 if my:
198 opened_by = [self._rhodecode_user.user_id]
198 opened_by = [self._rhodecode_user.user_id]
199
199
200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
200 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
201 if closed:
201 if closed:
202 statuses = [PullRequest.STATUS_CLOSED]
202 statuses = [PullRequest.STATUS_CLOSED]
203
203
204 data = self._get_pull_requests_list(
204 data = self._get_pull_requests_list(
205 repo_name=self.db_repo_name, source=source,
205 repo_name=self.db_repo_name, source=source,
206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
206 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
207
207
208 return data
208 return data
209
209
210 def _is_diff_cache_enabled(self, target_repo):
210 def _is_diff_cache_enabled(self, target_repo):
211 caching_enabled = self._get_general_setting(
211 caching_enabled = self._get_general_setting(
212 target_repo, 'rhodecode_diff_cache')
212 target_repo, 'rhodecode_diff_cache')
213 log.debug('Diff caching enabled: %s', caching_enabled)
213 log.debug('Diff caching enabled: %s', caching_enabled)
214 return caching_enabled
214 return caching_enabled
215
215
216 def _get_diffset(self, source_repo_name, source_repo,
216 def _get_diffset(self, source_repo_name, source_repo,
217 ancestor_commit,
217 ancestor_commit,
218 source_ref_id, target_ref_id,
218 source_ref_id, target_ref_id,
219 target_commit, source_commit, diff_limit, file_limit,
219 target_commit, source_commit, diff_limit, file_limit,
220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
220 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
221
221
222 if use_ancestor:
222 if use_ancestor:
223 # we might want to not use it for versions
223 # we might want to not use it for versions
224 target_ref_id = ancestor_commit.raw_id
224 target_ref_id = ancestor_commit.raw_id
225
225
226 vcs_diff = PullRequestModel().get_diff(
226 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
227 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
228 hide_whitespace_changes, diff_context)
229
229
230 diff_processor = diffs.DiffProcessor(
230 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
232 file_limit=file_limit, show_full_diff=fulldiff)
233
233
234 _parsed = diff_processor.prepare()
234 _parsed = diff_processor.prepare()
235
235
236 diffset = codeblocks.DiffSet(
236 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
237 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
238 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
239 source_node_getter=codeblocks.diffset_node_getter(target_commit),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit),
241 )
241 )
242 diffset = self.path_filter.render_patchset_filtered(
242 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
243 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
244
244
245 return diffset
245 return diffset
246
246
247 def _get_range_diffset(self, source_scm, source_repo,
247 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
248 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
249 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
250 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
251 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
252 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
253 context=diff_context)
254
254
255 diff_processor = diffs.DiffProcessor(
255 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
257 file_limit=file_limit, show_full_diff=fulldiff)
258
258
259 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
260
260
261 diffset = codeblocks.DiffSet(
261 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
262 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
265
266 diffset = self.path_filter.render_patchset_filtered(
266 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
268
269 return diffset
269 return diffset
270
270
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
272 comments_model = CommentsModel()
273
273
274 # GENERAL COMMENTS with versions #
274 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
276 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
277 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
278 q = q.filter(ChangesetComment.draft == false())
279 general_comments = q
279 general_comments = q
280
280
281 # pick comments we want to render at current version
281 # pick comments we want to render at current version
282 c.comment_versions = comments_model.aggregate_comments(
282 c.comment_versions = comments_model.aggregate_comments(
283 general_comments, versions, c.at_version_num)
283 general_comments, versions, c.at_version_num)
284
284
285 # INLINE COMMENTS with versions #
285 # INLINE COMMENTS with versions #
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = q.order_by(ChangesetComment.comment_id.asc())
287 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
288 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
289 q = q.filter(ChangesetComment.draft == false())
290 inline_comments = q
290 inline_comments = q
291
291
292 c.inline_versions = comments_model.aggregate_comments(
292 c.inline_versions = comments_model.aggregate_comments(
293 inline_comments, versions, c.at_version_num, inline=True)
293 inline_comments, versions, c.at_version_num, inline=True)
294
294
295 # Comments inline+general
295 # Comments inline+general
296 if c.at_version:
296 if c.at_version:
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
299 else:
299 else:
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
302
302
303 return general_comments, inline_comments
303 return general_comments, inline_comments
304
304
305 @LoginRequired()
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
306 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
307 'repository.read', 'repository.write', 'repository.admin')
308 @view_config(
308 @view_config(
309 route_name='pullrequest_show', request_method='GET',
309 route_name='pullrequest_show', request_method='GET',
310 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
310 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
311 def pull_request_show(self):
311 def pull_request_show(self):
312 _ = self.request.translate
312 _ = self.request.translate
313 c = self.load_default_context()
313 c = self.load_default_context()
314
314
315 pull_request = PullRequest.get_or_404(
315 pull_request = PullRequest.get_or_404(
316 self.request.matchdict['pull_request_id'])
316 self.request.matchdict['pull_request_id'])
317 pull_request_id = pull_request.pull_request_id
317 pull_request_id = pull_request.pull_request_id
318
318
319 c.state_progressing = pull_request.is_state_changing()
319 c.state_progressing = pull_request.is_state_changing()
320 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
320 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
321
321
322 _new_state = {
322 _new_state = {
323 'created': PullRequest.STATE_CREATED,
323 'created': PullRequest.STATE_CREATED,
324 }.get(self.request.GET.get('force_state'))
324 }.get(self.request.GET.get('force_state'))
325
325
326 if c.is_super_admin and _new_state:
326 if c.is_super_admin and _new_state:
327 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
327 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
328 h.flash(
328 h.flash(
329 _('Pull Request state was force changed to `{}`').format(_new_state),
329 _('Pull Request state was force changed to `{}`').format(_new_state),
330 category='success')
330 category='success')
331 Session().commit()
331 Session().commit()
332
332
333 raise HTTPFound(h.route_path(
333 raise HTTPFound(h.route_path(
334 'pullrequest_show', repo_name=self.db_repo_name,
334 'pullrequest_show', repo_name=self.db_repo_name,
335 pull_request_id=pull_request_id))
335 pull_request_id=pull_request_id))
336
336
337 version = self.request.GET.get('version')
337 version = self.request.GET.get('version')
338 from_version = self.request.GET.get('from_version') or version
338 from_version = self.request.GET.get('from_version') or version
339 merge_checks = self.request.GET.get('merge_checks')
339 merge_checks = self.request.GET.get('merge_checks')
340 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
340 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
341 force_refresh = str2bool(self.request.GET.get('force_refresh'))
341 force_refresh = str2bool(self.request.GET.get('force_refresh'))
342 c.range_diff_on = self.request.GET.get('range-diff') == "1"
342 c.range_diff_on = self.request.GET.get('range-diff') == "1"
343
343
344 # fetch global flags of ignore ws or context lines
344 # fetch global flags of ignore ws or context lines
345 diff_context = diffs.get_diff_context(self.request)
345 diff_context = diffs.get_diff_context(self.request)
346 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
346 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
347
347
348 (pull_request_latest,
348 (pull_request_latest,
349 pull_request_at_ver,
349 pull_request_at_ver,
350 pull_request_display_obj,
350 pull_request_display_obj,
351 at_version) = PullRequestModel().get_pr_version(
351 at_version) = PullRequestModel().get_pr_version(
352 pull_request_id, version=version)
352 pull_request_id, version=version)
353
353
354 pr_closed = pull_request_latest.is_closed()
354 pr_closed = pull_request_latest.is_closed()
355
355
356 if pr_closed and (version or from_version):
356 if pr_closed and (version or from_version):
357 # not allow to browse versions for closed PR
357 # not allow to browse versions for closed PR
358 raise HTTPFound(h.route_path(
358 raise HTTPFound(h.route_path(
359 'pullrequest_show', repo_name=self.db_repo_name,
359 'pullrequest_show', repo_name=self.db_repo_name,
360 pull_request_id=pull_request_id))
360 pull_request_id=pull_request_id))
361
361
362 versions = pull_request_display_obj.versions()
362 versions = pull_request_display_obj.versions()
363 # used to store per-commit range diffs
363 # used to store per-commit range diffs
364 c.changes = collections.OrderedDict()
364 c.changes = collections.OrderedDict()
365
365
366 c.at_version = at_version
366 c.at_version = at_version
367 c.at_version_num = (at_version
367 c.at_version_num = (at_version
368 if at_version and at_version != PullRequest.LATEST_VER
368 if at_version and at_version != PullRequest.LATEST_VER
369 else None)
369 else None)
370
370
371 c.at_version_index = ChangesetComment.get_index_from_version(
371 c.at_version_index = ChangesetComment.get_index_from_version(
372 c.at_version_num, versions)
372 c.at_version_num, versions)
373
373
374 (prev_pull_request_latest,
374 (prev_pull_request_latest,
375 prev_pull_request_at_ver,
375 prev_pull_request_at_ver,
376 prev_pull_request_display_obj,
376 prev_pull_request_display_obj,
377 prev_at_version) = PullRequestModel().get_pr_version(
377 prev_at_version) = PullRequestModel().get_pr_version(
378 pull_request_id, version=from_version)
378 pull_request_id, version=from_version)
379
379
380 c.from_version = prev_at_version
380 c.from_version = prev_at_version
381 c.from_version_num = (prev_at_version
381 c.from_version_num = (prev_at_version
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 else None)
383 else None)
384 c.from_version_index = ChangesetComment.get_index_from_version(
384 c.from_version_index = ChangesetComment.get_index_from_version(
385 c.from_version_num, versions)
385 c.from_version_num, versions)
386
386
387 # define if we're in COMPARE mode or VIEW at version mode
387 # define if we're in COMPARE mode or VIEW at version mode
388 compare = at_version != prev_at_version
388 compare = at_version != prev_at_version
389
389
390 # pull_requests repo_name we opened it against
390 # pull_requests repo_name we opened it against
391 # ie. target_repo must match
391 # ie. target_repo must match
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 log.warning('Mismatch between the current repo: %s, and target %s',
393 log.warning('Mismatch between the current repo: %s, and target %s',
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 raise HTTPNotFound()
395 raise HTTPNotFound()
396
396
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398
398
399 c.pull_request = pull_request_display_obj
399 c.pull_request = pull_request_display_obj
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 c.pull_request_latest = pull_request_latest
401 c.pull_request_latest = pull_request_latest
402
402
403 # inject latest version
403 # inject latest version
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 c.versions = versions + [latest_ver]
405 c.versions = versions + [latest_ver]
406
406
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 c.allowed_to_change_status = False
408 c.allowed_to_change_status = False
409 c.allowed_to_update = False
409 c.allowed_to_update = False
410 c.allowed_to_merge = False
410 c.allowed_to_merge = False
411 c.allowed_to_delete = False
411 c.allowed_to_delete = False
412 c.allowed_to_comment = False
412 c.allowed_to_comment = False
413 c.allowed_to_close = False
413 c.allowed_to_close = False
414 else:
414 else:
415 can_change_status = PullRequestModel().check_user_change_status(
415 can_change_status = PullRequestModel().check_user_change_status(
416 pull_request_at_ver, self._rhodecode_user)
416 pull_request_at_ver, self._rhodecode_user)
417 c.allowed_to_change_status = can_change_status and not pr_closed
417 c.allowed_to_change_status = can_change_status and not pr_closed
418
418
419 c.allowed_to_update = PullRequestModel().check_user_update(
419 c.allowed_to_update = PullRequestModel().check_user_update(
420 pull_request_latest, self._rhodecode_user) and not pr_closed
420 pull_request_latest, self._rhodecode_user) and not pr_closed
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 pull_request_latest, self._rhodecode_user) and not pr_closed
422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 pull_request_latest, self._rhodecode_user) and not pr_closed
424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 c.allowed_to_comment = not pr_closed
425 c.allowed_to_comment = not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427
427
428 c.forbid_adding_reviewers = False
428 c.forbid_adding_reviewers = False
429 c.forbid_author_to_review = False
429 c.forbid_author_to_review = False
430 c.forbid_commit_author_to_review = False
430 c.forbid_commit_author_to_review = False
431
431
432 if pull_request_latest.reviewer_data and \
432 if pull_request_latest.reviewer_data and \
433 'rules' in pull_request_latest.reviewer_data:
433 'rules' in pull_request_latest.reviewer_data:
434 rules = pull_request_latest.reviewer_data['rules'] or {}
434 rules = pull_request_latest.reviewer_data['rules'] or {}
435 try:
435 try:
436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 c.forbid_author_to_review = rules.get('forbid_author_to_review')
437 c.forbid_author_to_review = rules.get('forbid_author_to_review')
438 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
438 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
439 except Exception:
439 except Exception:
440 pass
440 pass
441
441
442 # check merge capabilities
442 # check merge capabilities
443 _merge_check = MergeCheck.validate(
443 _merge_check = MergeCheck.validate(
444 pull_request_latest, auth_user=self._rhodecode_user,
444 pull_request_latest, auth_user=self._rhodecode_user,
445 translator=self.request.translate,
445 translator=self.request.translate,
446 force_shadow_repo_refresh=force_refresh)
446 force_shadow_repo_refresh=force_refresh)
447
447
448 c.pr_merge_errors = _merge_check.error_details
448 c.pr_merge_errors = _merge_check.error_details
449 c.pr_merge_possible = not _merge_check.failed
449 c.pr_merge_possible = not _merge_check.failed
450 c.pr_merge_message = _merge_check.merge_msg
450 c.pr_merge_message = _merge_check.merge_msg
451 c.pr_merge_source_commit = _merge_check.source_commit
451 c.pr_merge_source_commit = _merge_check.source_commit
452 c.pr_merge_target_commit = _merge_check.target_commit
452 c.pr_merge_target_commit = _merge_check.target_commit
453
453
454 c.pr_merge_info = MergeCheck.get_merge_conditions(
454 c.pr_merge_info = MergeCheck.get_merge_conditions(
455 pull_request_latest, translator=self.request.translate)
455 pull_request_latest, translator=self.request.translate)
456
456
457 c.pull_request_review_status = _merge_check.review_status
457 c.pull_request_review_status = _merge_check.review_status
458 if merge_checks:
458 if merge_checks:
459 self.request.override_renderer = \
459 self.request.override_renderer = \
460 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
460 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
461 return self._get_template_context(c)
461 return self._get_template_context(c)
462
462
463 c.reviewers_count = pull_request.reviewers_count
463 c.reviewers_count = pull_request.reviewers_count
464 c.observers_count = pull_request.observers_count
464 c.observers_count = pull_request.observers_count
465
465
466 # reviewers and statuses
466 # reviewers and statuses
467 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
467 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
468 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
468 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
469 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
469 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
470
470
471 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
471 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
472 member_reviewer = h.reviewer_as_json(
472 member_reviewer = h.reviewer_as_json(
473 member, reasons=reasons, mandatory=mandatory,
473 member, reasons=reasons, mandatory=mandatory,
474 role=review_obj.role,
474 role=review_obj.role,
475 user_group=review_obj.rule_user_group_data()
475 user_group=review_obj.rule_user_group_data()
476 )
476 )
477
477
478 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
478 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
479 member_reviewer['review_status'] = current_review_status
479 member_reviewer['review_status'] = current_review_status
480 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
480 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
481 member_reviewer['allowed_to_update'] = c.allowed_to_update
481 member_reviewer['allowed_to_update'] = c.allowed_to_update
482 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
482 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
483
483
484 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
484 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
485
485
486 for observer_obj, member in pull_request_at_ver.observers():
486 for observer_obj, member in pull_request_at_ver.observers():
487 member_observer = h.reviewer_as_json(
487 member_observer = h.reviewer_as_json(
488 member, reasons=[], mandatory=False,
488 member, reasons=[], mandatory=False,
489 role=observer_obj.role,
489 role=observer_obj.role,
490 user_group=observer_obj.rule_user_group_data()
490 user_group=observer_obj.rule_user_group_data()
491 )
491 )
492 member_observer['allowed_to_update'] = c.allowed_to_update
492 member_observer['allowed_to_update'] = c.allowed_to_update
493 c.pull_request_set_observers_data_json['observers'].append(member_observer)
493 c.pull_request_set_observers_data_json['observers'].append(member_observer)
494
494
495 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
495 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
496
496
497 general_comments, inline_comments = \
497 general_comments, inline_comments = \
498 self.register_comments_vars(c, pull_request_latest, versions)
498 self.register_comments_vars(c, pull_request_latest, versions)
499
499
500 # TODOs
500 # TODOs
501 c.unresolved_comments = CommentsModel() \
501 c.unresolved_comments = CommentsModel() \
502 .get_pull_request_unresolved_todos(pull_request_latest)
502 .get_pull_request_unresolved_todos(pull_request_latest)
503 c.resolved_comments = CommentsModel() \
503 c.resolved_comments = CommentsModel() \
504 .get_pull_request_resolved_todos(pull_request_latest)
504 .get_pull_request_resolved_todos(pull_request_latest)
505
505
506 # if we use version, then do not show later comments
506 # if we use version, then do not show later comments
507 # than current version
507 # than current version
508 display_inline_comments = collections.defaultdict(
508 display_inline_comments = collections.defaultdict(
509 lambda: collections.defaultdict(list))
509 lambda: collections.defaultdict(list))
510 for co in inline_comments:
510 for co in inline_comments:
511 if c.at_version_num:
511 if c.at_version_num:
512 # pick comments that are at least UPTO given version, so we
512 # pick comments that are at least UPTO given version, so we
513 # don't render comments for higher version
513 # don't render comments for higher version
514 should_render = co.pull_request_version_id and \
514 should_render = co.pull_request_version_id and \
515 co.pull_request_version_id <= c.at_version_num
515 co.pull_request_version_id <= c.at_version_num
516 else:
516 else:
517 # showing all, for 'latest'
517 # showing all, for 'latest'
518 should_render = True
518 should_render = True
519
519
520 if should_render:
520 if should_render:
521 display_inline_comments[co.f_path][co.line_no].append(co)
521 display_inline_comments[co.f_path][co.line_no].append(co)
522
522
523 # load diff data into template context, if we use compare mode then
523 # load diff data into template context, if we use compare mode then
524 # diff is calculated based on changes between versions of PR
524 # diff is calculated based on changes between versions of PR
525
525
526 source_repo = pull_request_at_ver.source_repo
526 source_repo = pull_request_at_ver.source_repo
527 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
527 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
528
528
529 target_repo = pull_request_at_ver.target_repo
529 target_repo = pull_request_at_ver.target_repo
530 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
530 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
531
531
532 if compare:
532 if compare:
533 # in compare switch the diff base to latest commit from prev version
533 # in compare switch the diff base to latest commit from prev version
534 target_ref_id = prev_pull_request_display_obj.revisions[0]
534 target_ref_id = prev_pull_request_display_obj.revisions[0]
535
535
536 # despite opening commits for bookmarks/branches/tags, we always
536 # despite opening commits for bookmarks/branches/tags, we always
537 # convert this to rev to prevent changes after bookmark or branch change
537 # convert this to rev to prevent changes after bookmark or branch change
538 c.source_ref_type = 'rev'
538 c.source_ref_type = 'rev'
539 c.source_ref = source_ref_id
539 c.source_ref = source_ref_id
540
540
541 c.target_ref_type = 'rev'
541 c.target_ref_type = 'rev'
542 c.target_ref = target_ref_id
542 c.target_ref = target_ref_id
543
543
544 c.source_repo = source_repo
544 c.source_repo = source_repo
545 c.target_repo = target_repo
545 c.target_repo = target_repo
546
546
547 c.commit_ranges = []
547 c.commit_ranges = []
548 source_commit = EmptyCommit()
548 source_commit = EmptyCommit()
549 target_commit = EmptyCommit()
549 target_commit = EmptyCommit()
550 c.missing_requirements = False
550 c.missing_requirements = False
551
551
552 source_scm = source_repo.scm_instance()
552 source_scm = source_repo.scm_instance()
553 target_scm = target_repo.scm_instance()
553 target_scm = target_repo.scm_instance()
554
554
555 shadow_scm = None
555 shadow_scm = None
556 try:
556 try:
557 shadow_scm = pull_request_latest.get_shadow_repo()
557 shadow_scm = pull_request_latest.get_shadow_repo()
558 except Exception:
558 except Exception:
559 log.debug('Failed to get shadow repo', exc_info=True)
559 log.debug('Failed to get shadow repo', exc_info=True)
560 # try first the existing source_repo, and then shadow
560 # try first the existing source_repo, and then shadow
561 # repo if we can obtain one
561 # repo if we can obtain one
562 commits_source_repo = source_scm
562 commits_source_repo = source_scm
563 if shadow_scm:
563 if shadow_scm:
564 commits_source_repo = shadow_scm
564 commits_source_repo = shadow_scm
565
565
566 c.commits_source_repo = commits_source_repo
566 c.commits_source_repo = commits_source_repo
567 c.ancestor = None # set it to None, to hide it from PR view
567 c.ancestor = None # set it to None, to hide it from PR view
568
568
569 # empty version means latest, so we keep this to prevent
569 # empty version means latest, so we keep this to prevent
570 # double caching
570 # double caching
571 version_normalized = version or PullRequest.LATEST_VER
571 version_normalized = version or PullRequest.LATEST_VER
572 from_version_normalized = from_version or PullRequest.LATEST_VER
572 from_version_normalized = from_version or PullRequest.LATEST_VER
573
573
574 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
574 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
575 cache_file_path = diff_cache_exist(
575 cache_file_path = diff_cache_exist(
576 cache_path, 'pull_request', pull_request_id, version_normalized,
576 cache_path, 'pull_request', pull_request_id, version_normalized,
577 from_version_normalized, source_ref_id, target_ref_id,
577 from_version_normalized, source_ref_id, target_ref_id,
578 hide_whitespace_changes, diff_context, c.fulldiff)
578 hide_whitespace_changes, diff_context, c.fulldiff)
579
579
580 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
580 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
581 force_recache = self.get_recache_flag()
581 force_recache = self.get_recache_flag()
582
582
583 cached_diff = None
583 cached_diff = None
584 if caching_enabled:
584 if caching_enabled:
585 cached_diff = load_cached_diff(cache_file_path)
585 cached_diff = load_cached_diff(cache_file_path)
586
586
587 has_proper_commit_cache = (
587 has_proper_commit_cache = (
588 cached_diff and cached_diff.get('commits')
588 cached_diff and cached_diff.get('commits')
589 and len(cached_diff.get('commits', [])) == 5
589 and len(cached_diff.get('commits', [])) == 5
590 and cached_diff.get('commits')[0]
590 and cached_diff.get('commits')[0]
591 and cached_diff.get('commits')[3])
591 and cached_diff.get('commits')[3])
592
592
593 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
593 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
594 diff_commit_cache = \
594 diff_commit_cache = \
595 (ancestor_commit, commit_cache, missing_requirements,
595 (ancestor_commit, commit_cache, missing_requirements,
596 source_commit, target_commit) = cached_diff['commits']
596 source_commit, target_commit) = cached_diff['commits']
597 else:
597 else:
598 # NOTE(marcink): we reach potentially unreachable errors when a PR has
598 # NOTE(marcink): we reach potentially unreachable errors when a PR has
599 # merge errors resulting in potentially hidden commits in the shadow repo.
599 # merge errors resulting in potentially hidden commits in the shadow repo.
600 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
600 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
601 and _merge_check.merge_response
601 and _merge_check.merge_response
602 maybe_unreachable = maybe_unreachable \
602 maybe_unreachable = maybe_unreachable \
603 and _merge_check.merge_response.metadata.get('unresolved_files')
603 and _merge_check.merge_response.metadata.get('unresolved_files')
604 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
604 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
605 diff_commit_cache = \
605 diff_commit_cache = \
606 (ancestor_commit, commit_cache, missing_requirements,
606 (ancestor_commit, commit_cache, missing_requirements,
607 source_commit, target_commit) = self.get_commits(
607 source_commit, target_commit) = self.get_commits(
608 commits_source_repo,
608 commits_source_repo,
609 pull_request_at_ver,
609 pull_request_at_ver,
610 source_commit,
610 source_commit,
611 source_ref_id,
611 source_ref_id,
612 source_scm,
612 source_scm,
613 target_commit,
613 target_commit,
614 target_ref_id,
614 target_ref_id,
615 target_scm,
615 target_scm,
616 maybe_unreachable=maybe_unreachable)
616 maybe_unreachable=maybe_unreachable)
617
617
618 # register our commit range
618 # register our commit range
619 for comm in commit_cache.values():
619 for comm in commit_cache.values():
620 c.commit_ranges.append(comm)
620 c.commit_ranges.append(comm)
621
621
622 c.missing_requirements = missing_requirements
622 c.missing_requirements = missing_requirements
623 c.ancestor_commit = ancestor_commit
623 c.ancestor_commit = ancestor_commit
624 c.statuses = source_repo.statuses(
624 c.statuses = source_repo.statuses(
625 [x.raw_id for x in c.commit_ranges])
625 [x.raw_id for x in c.commit_ranges])
626
626
627 # auto collapse if we have more than limit
627 # auto collapse if we have more than limit
628 collapse_limit = diffs.DiffProcessor._collapse_commits_over
628 collapse_limit = diffs.DiffProcessor._collapse_commits_over
629 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
629 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
630 c.compare_mode = compare
630 c.compare_mode = compare
631
631
632 # diff_limit is the old behavior, will cut off the whole diff
632 # diff_limit is the old behavior, will cut off the whole diff
633 # if the limit is applied otherwise will just hide the
633 # if the limit is applied otherwise will just hide the
634 # big files from the front-end
634 # big files from the front-end
635 diff_limit = c.visual.cut_off_limit_diff
635 diff_limit = c.visual.cut_off_limit_diff
636 file_limit = c.visual.cut_off_limit_file
636 file_limit = c.visual.cut_off_limit_file
637
637
638 c.missing_commits = False
638 c.missing_commits = False
639 if (c.missing_requirements
639 if (c.missing_requirements
640 or isinstance(source_commit, EmptyCommit)
640 or isinstance(source_commit, EmptyCommit)
641 or source_commit == target_commit):
641 or source_commit == target_commit):
642
642
643 c.missing_commits = True
643 c.missing_commits = True
644 else:
644 else:
645 c.inline_comments = display_inline_comments
645 c.inline_comments = display_inline_comments
646
646
647 use_ancestor = True
647 use_ancestor = True
648 if from_version_normalized != version_normalized:
648 if from_version_normalized != version_normalized:
649 use_ancestor = False
649 use_ancestor = False
650
650
651 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
651 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
652 if not force_recache and has_proper_diff_cache:
652 if not force_recache and has_proper_diff_cache:
653 c.diffset = cached_diff['diff']
653 c.diffset = cached_diff['diff']
654 else:
654 else:
655 try:
655 try:
656 c.diffset = self._get_diffset(
656 c.diffset = self._get_diffset(
657 c.source_repo.repo_name, commits_source_repo,
657 c.source_repo.repo_name, commits_source_repo,
658 c.ancestor_commit,
658 c.ancestor_commit,
659 source_ref_id, target_ref_id,
659 source_ref_id, target_ref_id,
660 target_commit, source_commit,
660 target_commit, source_commit,
661 diff_limit, file_limit, c.fulldiff,
661 diff_limit, file_limit, c.fulldiff,
662 hide_whitespace_changes, diff_context,
662 hide_whitespace_changes, diff_context,
663 use_ancestor=use_ancestor
663 use_ancestor=use_ancestor
664 )
664 )
665
665
666 # save cached diff
666 # save cached diff
667 if caching_enabled:
667 if caching_enabled:
668 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
668 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
669 except CommitDoesNotExistError:
669 except CommitDoesNotExistError:
670 log.exception('Failed to generate diffset')
670 log.exception('Failed to generate diffset')
671 c.missing_commits = True
671 c.missing_commits = True
672
672
673 if not c.missing_commits:
673 if not c.missing_commits:
674
674
675 c.limited_diff = c.diffset.limited_diff
675 c.limited_diff = c.diffset.limited_diff
676
676
677 # calculate removed files that are bound to comments
677 # calculate removed files that are bound to comments
678 comment_deleted_files = [
678 comment_deleted_files = [
679 fname for fname in display_inline_comments
679 fname for fname in display_inline_comments
680 if fname not in c.diffset.file_stats]
680 if fname not in c.diffset.file_stats]
681
681
682 c.deleted_files_comments = collections.defaultdict(dict)
682 c.deleted_files_comments = collections.defaultdict(dict)
683 for fname, per_line_comments in display_inline_comments.items():
683 for fname, per_line_comments in display_inline_comments.items():
684 if fname in comment_deleted_files:
684 if fname in comment_deleted_files:
685 c.deleted_files_comments[fname]['stats'] = 0
685 c.deleted_files_comments[fname]['stats'] = 0
686 c.deleted_files_comments[fname]['comments'] = list()
686 c.deleted_files_comments[fname]['comments'] = list()
687 for lno, comments in per_line_comments.items():
687 for lno, comments in per_line_comments.items():
688 c.deleted_files_comments[fname]['comments'].extend(comments)
688 c.deleted_files_comments[fname]['comments'].extend(comments)
689
689
690 # maybe calculate the range diff
690 # maybe calculate the range diff
691 if c.range_diff_on:
691 if c.range_diff_on:
692 # TODO(marcink): set whitespace/context
692 # TODO(marcink): set whitespace/context
693 context_lcl = 3
693 context_lcl = 3
694 ign_whitespace_lcl = False
694 ign_whitespace_lcl = False
695
695
696 for commit in c.commit_ranges:
696 for commit in c.commit_ranges:
697 commit2 = commit
697 commit2 = commit
698 commit1 = commit.first_parent
698 commit1 = commit.first_parent
699
699
700 range_diff_cache_file_path = diff_cache_exist(
700 range_diff_cache_file_path = diff_cache_exist(
701 cache_path, 'diff', commit.raw_id,
701 cache_path, 'diff', commit.raw_id,
702 ign_whitespace_lcl, context_lcl, c.fulldiff)
702 ign_whitespace_lcl, context_lcl, c.fulldiff)
703
703
704 cached_diff = None
704 cached_diff = None
705 if caching_enabled:
705 if caching_enabled:
706 cached_diff = load_cached_diff(range_diff_cache_file_path)
706 cached_diff = load_cached_diff(range_diff_cache_file_path)
707
707
708 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
708 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
709 if not force_recache and has_proper_diff_cache:
709 if not force_recache and has_proper_diff_cache:
710 diffset = cached_diff['diff']
710 diffset = cached_diff['diff']
711 else:
711 else:
712 diffset = self._get_range_diffset(
712 diffset = self._get_range_diffset(
713 commits_source_repo, source_repo,
713 commits_source_repo, source_repo,
714 commit1, commit2, diff_limit, file_limit,
714 commit1, commit2, diff_limit, file_limit,
715 c.fulldiff, ign_whitespace_lcl, context_lcl
715 c.fulldiff, ign_whitespace_lcl, context_lcl
716 )
716 )
717
717
718 # save cached diff
718 # save cached diff
719 if caching_enabled:
719 if caching_enabled:
720 cache_diff(range_diff_cache_file_path, diffset, None)
720 cache_diff(range_diff_cache_file_path, diffset, None)
721
721
722 c.changes[commit.raw_id] = diffset
722 c.changes[commit.raw_id] = diffset
723
723
724 # this is a hack to properly display links, when creating PR, the
724 # this is a hack to properly display links, when creating PR, the
725 # compare view and others uses different notation, and
725 # compare view and others uses different notation, and
726 # compare_commits.mako renders links based on the target_repo.
726 # compare_commits.mako renders links based on the target_repo.
727 # We need to swap that here to generate it properly on the html side
727 # We need to swap that here to generate it properly on the html side
728 c.target_repo = c.source_repo
728 c.target_repo = c.source_repo
729
729
730 c.commit_statuses = ChangesetStatus.STATUSES
730 c.commit_statuses = ChangesetStatus.STATUSES
731
731
732 c.show_version_changes = not pr_closed
732 c.show_version_changes = not pr_closed
733 if c.show_version_changes:
733 if c.show_version_changes:
734 cur_obj = pull_request_at_ver
734 cur_obj = pull_request_at_ver
735 prev_obj = prev_pull_request_at_ver
735 prev_obj = prev_pull_request_at_ver
736
736
737 old_commit_ids = prev_obj.revisions
737 old_commit_ids = prev_obj.revisions
738 new_commit_ids = cur_obj.revisions
738 new_commit_ids = cur_obj.revisions
739 commit_changes = PullRequestModel()._calculate_commit_id_changes(
739 commit_changes = PullRequestModel()._calculate_commit_id_changes(
740 old_commit_ids, new_commit_ids)
740 old_commit_ids, new_commit_ids)
741 c.commit_changes_summary = commit_changes
741 c.commit_changes_summary = commit_changes
742
742
743 # calculate the diff for commits between versions
743 # calculate the diff for commits between versions
744 c.commit_changes = []
744 c.commit_changes = []
745
745
746 def mark(cs, fw):
746 def mark(cs, fw):
747 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
747 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
748
748
749 for c_type, raw_id in mark(commit_changes.added, 'a') \
749 for c_type, raw_id in mark(commit_changes.added, 'a') \
750 + mark(commit_changes.removed, 'r') \
750 + mark(commit_changes.removed, 'r') \
751 + mark(commit_changes.common, 'c'):
751 + mark(commit_changes.common, 'c'):
752
752
753 if raw_id in commit_cache:
753 if raw_id in commit_cache:
754 commit = commit_cache[raw_id]
754 commit = commit_cache[raw_id]
755 else:
755 else:
756 try:
756 try:
757 commit = commits_source_repo.get_commit(raw_id)
757 commit = commits_source_repo.get_commit(raw_id)
758 except CommitDoesNotExistError:
758 except CommitDoesNotExistError:
759 # in case we fail extracting still use "dummy" commit
759 # in case we fail extracting still use "dummy" commit
760 # for display in commit diff
760 # for display in commit diff
761 commit = h.AttributeDict(
761 commit = h.AttributeDict(
762 {'raw_id': raw_id,
762 {'raw_id': raw_id,
763 'message': 'EMPTY or MISSING COMMIT'})
763 'message': 'EMPTY or MISSING COMMIT'})
764 c.commit_changes.append([c_type, commit])
764 c.commit_changes.append([c_type, commit])
765
765
766 # current user review statuses for each version
766 # current user review statuses for each version
767 c.review_versions = {}
767 c.review_versions = {}
768 is_reviewer = PullRequestModel().is_user_reviewer(
768 is_reviewer = PullRequestModel().is_user_reviewer(
769 pull_request, self._rhodecode_user)
769 pull_request, self._rhodecode_user)
770 if is_reviewer:
770 if is_reviewer:
771 for co in general_comments:
771 for co in general_comments:
772 if co.author.user_id == self._rhodecode_user.user_id:
772 if co.author.user_id == self._rhodecode_user.user_id:
773 status = co.status_change
773 status = co.status_change
774 if status:
774 if status:
775 _ver_pr = status[0].comment.pull_request_version_id
775 _ver_pr = status[0].comment.pull_request_version_id
776 c.review_versions[_ver_pr] = status[0]
776 c.review_versions[_ver_pr] = status[0]
777
777
778 return self._get_template_context(c)
778 return self._get_template_context(c)
779
779
780 def get_commits(
780 def get_commits(
781 self, commits_source_repo, pull_request_at_ver, source_commit,
781 self, commits_source_repo, pull_request_at_ver, source_commit,
782 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
782 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
783 maybe_unreachable=False):
783 maybe_unreachable=False):
784
784
785 commit_cache = collections.OrderedDict()
785 commit_cache = collections.OrderedDict()
786 missing_requirements = False
786 missing_requirements = False
787
787
788 try:
788 try:
789 pre_load = ["author", "date", "message", "branch", "parents"]
789 pre_load = ["author", "date", "message", "branch", "parents"]
790
790
791 pull_request_commits = pull_request_at_ver.revisions
791 pull_request_commits = pull_request_at_ver.revisions
792 log.debug('Loading %s commits from %s',
792 log.debug('Loading %s commits from %s',
793 len(pull_request_commits), commits_source_repo)
793 len(pull_request_commits), commits_source_repo)
794
794
795 for rev in pull_request_commits:
795 for rev in pull_request_commits:
796 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
796 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
797 maybe_unreachable=maybe_unreachable)
797 maybe_unreachable=maybe_unreachable)
798 commit_cache[comm.raw_id] = comm
798 commit_cache[comm.raw_id] = comm
799
799
800 # Order here matters, we first need to get target, and then
800 # Order here matters, we first need to get target, and then
801 # the source
801 # the source
802 target_commit = commits_source_repo.get_commit(
802 target_commit = commits_source_repo.get_commit(
803 commit_id=safe_str(target_ref_id))
803 commit_id=safe_str(target_ref_id))
804
804
805 source_commit = commits_source_repo.get_commit(
805 source_commit = commits_source_repo.get_commit(
806 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
806 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
807 except CommitDoesNotExistError:
807 except CommitDoesNotExistError:
808 log.warning('Failed to get commit from `{}` repo'.format(
808 log.warning('Failed to get commit from `{}` repo'.format(
809 commits_source_repo), exc_info=True)
809 commits_source_repo), exc_info=True)
810 except RepositoryRequirementError:
810 except RepositoryRequirementError:
811 log.warning('Failed to get all required data from repo', exc_info=True)
811 log.warning('Failed to get all required data from repo', exc_info=True)
812 missing_requirements = True
812 missing_requirements = True
813
813
814 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
814 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
815
815
816 try:
816 try:
817 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
817 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
818 except Exception:
818 except Exception:
819 ancestor_commit = None
819 ancestor_commit = None
820
820
821 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
821 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
822
822
823 def assure_not_empty_repo(self):
823 def assure_not_empty_repo(self):
824 _ = self.request.translate
824 _ = self.request.translate
825
825
826 try:
826 try:
827 self.db_repo.scm_instance().get_commit()
827 self.db_repo.scm_instance().get_commit()
828 except EmptyRepositoryError:
828 except EmptyRepositoryError:
829 h.flash(h.literal(_('There are no commits yet')),
829 h.flash(h.literal(_('There are no commits yet')),
830 category='warning')
830 category='warning')
831 raise HTTPFound(
831 raise HTTPFound(
832 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
832 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
833
833
834 @LoginRequired()
834 @LoginRequired()
835 @NotAnonymous()
835 @NotAnonymous()
836 @HasRepoPermissionAnyDecorator(
836 @HasRepoPermissionAnyDecorator(
837 'repository.read', 'repository.write', 'repository.admin')
837 'repository.read', 'repository.write', 'repository.admin')
838 @view_config(
838 @view_config(
839 route_name='pullrequest_new', request_method='GET',
839 route_name='pullrequest_new', request_method='GET',
840 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
840 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
841 def pull_request_new(self):
841 def pull_request_new(self):
842 _ = self.request.translate
842 _ = self.request.translate
843 c = self.load_default_context()
843 c = self.load_default_context()
844
844
845 self.assure_not_empty_repo()
845 self.assure_not_empty_repo()
846 source_repo = self.db_repo
846 source_repo = self.db_repo
847
847
848 commit_id = self.request.GET.get('commit')
848 commit_id = self.request.GET.get('commit')
849 branch_ref = self.request.GET.get('branch')
849 branch_ref = self.request.GET.get('branch')
850 bookmark_ref = self.request.GET.get('bookmark')
850 bookmark_ref = self.request.GET.get('bookmark')
851
851
852 try:
852 try:
853 source_repo_data = PullRequestModel().generate_repo_data(
853 source_repo_data = PullRequestModel().generate_repo_data(
854 source_repo, commit_id=commit_id,
854 source_repo, commit_id=commit_id,
855 branch=branch_ref, bookmark=bookmark_ref,
855 branch=branch_ref, bookmark=bookmark_ref,
856 translator=self.request.translate)
856 translator=self.request.translate)
857 except CommitDoesNotExistError as e:
857 except CommitDoesNotExistError as e:
858 log.exception(e)
858 log.exception(e)
859 h.flash(_('Commit does not exist'), 'error')
859 h.flash(_('Commit does not exist'), 'error')
860 raise HTTPFound(
860 raise HTTPFound(
861 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
861 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
862
862
863 default_target_repo = source_repo
863 default_target_repo = source_repo
864
864
865 if source_repo.parent and c.has_origin_repo_read_perm:
865 if source_repo.parent and c.has_origin_repo_read_perm:
866 parent_vcs_obj = source_repo.parent.scm_instance()
866 parent_vcs_obj = source_repo.parent.scm_instance()
867 if parent_vcs_obj and not parent_vcs_obj.is_empty():
867 if parent_vcs_obj and not parent_vcs_obj.is_empty():
868 # change default if we have a parent repo
868 # change default if we have a parent repo
869 default_target_repo = source_repo.parent
869 default_target_repo = source_repo.parent
870
870
871 target_repo_data = PullRequestModel().generate_repo_data(
871 target_repo_data = PullRequestModel().generate_repo_data(
872 default_target_repo, translator=self.request.translate)
872 default_target_repo, translator=self.request.translate)
873
873
874 selected_source_ref = source_repo_data['refs']['selected_ref']
874 selected_source_ref = source_repo_data['refs']['selected_ref']
875 title_source_ref = ''
875 title_source_ref = ''
876 if selected_source_ref:
876 if selected_source_ref:
877 title_source_ref = selected_source_ref.split(':', 2)[1]
877 title_source_ref = selected_source_ref.split(':', 2)[1]
878 c.default_title = PullRequestModel().generate_pullrequest_title(
878 c.default_title = PullRequestModel().generate_pullrequest_title(
879 source=source_repo.repo_name,
879 source=source_repo.repo_name,
880 source_ref=title_source_ref,
880 source_ref=title_source_ref,
881 target=default_target_repo.repo_name
881 target=default_target_repo.repo_name
882 )
882 )
883
883
884 c.default_repo_data = {
884 c.default_repo_data = {
885 'source_repo_name': source_repo.repo_name,
885 'source_repo_name': source_repo.repo_name,
886 'source_refs_json': json.dumps(source_repo_data),
886 'source_refs_json': json.dumps(source_repo_data),
887 'target_repo_name': default_target_repo.repo_name,
887 'target_repo_name': default_target_repo.repo_name,
888 'target_refs_json': json.dumps(target_repo_data),
888 'target_refs_json': json.dumps(target_repo_data),
889 }
889 }
890 c.default_source_ref = selected_source_ref
890 c.default_source_ref = selected_source_ref
891
891
892 return self._get_template_context(c)
892 return self._get_template_context(c)
893
893
894 @LoginRequired()
894 @LoginRequired()
895 @NotAnonymous()
895 @NotAnonymous()
896 @HasRepoPermissionAnyDecorator(
896 @HasRepoPermissionAnyDecorator(
897 'repository.read', 'repository.write', 'repository.admin')
897 'repository.read', 'repository.write', 'repository.admin')
898 @view_config(
898 @view_config(
899 route_name='pullrequest_repo_refs', request_method='GET',
899 route_name='pullrequest_repo_refs', request_method='GET',
900 renderer='json_ext', xhr=True)
900 renderer='json_ext', xhr=True)
901 def pull_request_repo_refs(self):
901 def pull_request_repo_refs(self):
902 self.load_default_context()
902 self.load_default_context()
903 target_repo_name = self.request.matchdict['target_repo_name']
903 target_repo_name = self.request.matchdict['target_repo_name']
904 repo = Repository.get_by_repo_name(target_repo_name)
904 repo = Repository.get_by_repo_name(target_repo_name)
905 if not repo:
905 if not repo:
906 raise HTTPNotFound()
906 raise HTTPNotFound()
907
907
908 target_perm = HasRepoPermissionAny(
908 target_perm = HasRepoPermissionAny(
909 'repository.read', 'repository.write', 'repository.admin')(
909 'repository.read', 'repository.write', 'repository.admin')(
910 target_repo_name)
910 target_repo_name)
911 if not target_perm:
911 if not target_perm:
912 raise HTTPNotFound()
912 raise HTTPNotFound()
913
913
914 return PullRequestModel().generate_repo_data(
914 return PullRequestModel().generate_repo_data(
915 repo, translator=self.request.translate)
915 repo, translator=self.request.translate)
916
916
917 @LoginRequired()
917 @LoginRequired()
918 @NotAnonymous()
918 @NotAnonymous()
919 @HasRepoPermissionAnyDecorator(
919 @HasRepoPermissionAnyDecorator(
920 'repository.read', 'repository.write', 'repository.admin')
920 'repository.read', 'repository.write', 'repository.admin')
921 @view_config(
921 @view_config(
922 route_name='pullrequest_repo_targets', request_method='GET',
922 route_name='pullrequest_repo_targets', request_method='GET',
923 renderer='json_ext', xhr=True)
923 renderer='json_ext', xhr=True)
924 def pullrequest_repo_targets(self):
924 def pullrequest_repo_targets(self):
925 _ = self.request.translate
925 _ = self.request.translate
926 filter_query = self.request.GET.get('query')
926 filter_query = self.request.GET.get('query')
927
927
928 # get the parents
928 # get the parents
929 parent_target_repos = []
929 parent_target_repos = []
930 if self.db_repo.parent:
930 if self.db_repo.parent:
931 parents_query = Repository.query() \
931 parents_query = Repository.query() \
932 .order_by(func.length(Repository.repo_name)) \
932 .order_by(func.length(Repository.repo_name)) \
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934
934
935 if filter_query:
935 if filter_query:
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 parents_query = parents_query.filter(
937 parents_query = parents_query.filter(
938 Repository.repo_name.ilike(ilike_expression))
938 Repository.repo_name.ilike(ilike_expression))
939 parents = parents_query.limit(20).all()
939 parents = parents_query.limit(20).all()
940
940
941 for parent in parents:
941 for parent in parents:
942 parent_vcs_obj = parent.scm_instance()
942 parent_vcs_obj = parent.scm_instance()
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 parent_target_repos.append(parent)
944 parent_target_repos.append(parent)
945
945
946 # get other forks, and repo itself
946 # get other forks, and repo itself
947 query = Repository.query() \
947 query = Repository.query() \
948 .order_by(func.length(Repository.repo_name)) \
948 .order_by(func.length(Repository.repo_name)) \
949 .filter(
949 .filter(
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 ) \
952 ) \
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954
954
955 if filter_query:
955 if filter_query:
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958
958
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 target_repos = query.limit(limit).all()
960 target_repos = query.limit(limit).all()
961
961
962 all_target_repos = target_repos + parent_target_repos
962 all_target_repos = target_repos + parent_target_repos
963
963
964 repos = []
964 repos = []
965 # This checks permissions to the repositories
965 # This checks permissions to the repositories
966 for obj in ScmModel().get_repos(all_target_repos):
966 for obj in ScmModel().get_repos(all_target_repos):
967 repos.append({
967 repos.append({
968 'id': obj['name'],
968 'id': obj['name'],
969 'text': obj['name'],
969 'text': obj['name'],
970 'type': 'repo',
970 'type': 'repo',
971 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_id': obj['dbrepo']['repo_id'],
972 'repo_type': obj['dbrepo']['repo_type'],
972 'repo_type': obj['dbrepo']['repo_type'],
973 'private': obj['dbrepo']['private'],
973 'private': obj['dbrepo']['private'],
974
974
975 })
975 })
976
976
977 data = {
977 data = {
978 'more': False,
978 'more': False,
979 'results': [{
979 'results': [{
980 'text': _('Repositories'),
980 'text': _('Repositories'),
981 'children': repos
981 'children': repos
982 }] if repos else []
982 }] if repos else []
983 }
983 }
984 return data
984 return data
985
985
986 def _get_existing_ids(self, post_data):
986 @classmethod
987 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
987 def get_comment_ids(cls, post_data):
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988
989
989 @LoginRequired()
990 @LoginRequired()
990 @NotAnonymous()
991 @NotAnonymous()
991 @HasRepoPermissionAnyDecorator(
992 @HasRepoPermissionAnyDecorator(
992 'repository.read', 'repository.write', 'repository.admin')
993 'repository.read', 'repository.write', 'repository.admin')
993 @view_config(
994 @view_config(
994 route_name='pullrequest_comments', request_method='POST',
995 route_name='pullrequest_comments', request_method='POST',
995 renderer='string_html', xhr=True)
996 renderer='string_html', xhr=True)
996 def pullrequest_comments(self):
997 def pullrequest_comments(self):
997 self.load_default_context()
998 self.load_default_context()
998
999
999 pull_request = PullRequest.get_or_404(
1000 pull_request = PullRequest.get_or_404(
1000 self.request.matchdict['pull_request_id'])
1001 self.request.matchdict['pull_request_id'])
1001 pull_request_id = pull_request.pull_request_id
1002 pull_request_id = pull_request.pull_request_id
1002 version = self.request.GET.get('version')
1003 version = self.request.GET.get('version')
1003
1004
1004 _render = self.request.get_partial_renderer(
1005 _render = self.request.get_partial_renderer(
1005 'rhodecode:templates/base/sidebar.mako')
1006 'rhodecode:templates/base/sidebar.mako')
1006 c = _render.get_call_context()
1007 c = _render.get_call_context()
1007
1008
1008 (pull_request_latest,
1009 (pull_request_latest,
1009 pull_request_at_ver,
1010 pull_request_at_ver,
1010 pull_request_display_obj,
1011 pull_request_display_obj,
1011 at_version) = PullRequestModel().get_pr_version(
1012 at_version) = PullRequestModel().get_pr_version(
1012 pull_request_id, version=version)
1013 pull_request_id, version=version)
1013 versions = pull_request_display_obj.versions()
1014 versions = pull_request_display_obj.versions()
1014 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1015 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1015 c.versions = versions + [latest_ver]
1016 c.versions = versions + [latest_ver]
1016
1017
1017 c.at_version = at_version
1018 c.at_version = at_version
1018 c.at_version_num = (at_version
1019 c.at_version_num = (at_version
1019 if at_version and at_version != PullRequest.LATEST_VER
1020 if at_version and at_version != PullRequest.LATEST_VER
1020 else None)
1021 else None)
1021
1022
1022 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1023 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1023 all_comments = c.inline_comments_flat + c.comments
1024 all_comments = c.inline_comments_flat + c.comments
1024
1025
1025 existing_ids = self._get_existing_ids(self.request.POST)
1026 existing_ids = self.get_comment_ids(self.request.POST)
1026 return _render('comments_table', all_comments, len(all_comments),
1027 return _render('comments_table', all_comments, len(all_comments),
1027 existing_ids=existing_ids)
1028 existing_ids=existing_ids)
1028
1029
1029 @LoginRequired()
1030 @LoginRequired()
1030 @NotAnonymous()
1031 @NotAnonymous()
1031 @HasRepoPermissionAnyDecorator(
1032 @HasRepoPermissionAnyDecorator(
1032 'repository.read', 'repository.write', 'repository.admin')
1033 'repository.read', 'repository.write', 'repository.admin')
1033 @view_config(
1034 @view_config(
1034 route_name='pullrequest_todos', request_method='POST',
1035 route_name='pullrequest_todos', request_method='POST',
1035 renderer='string_html', xhr=True)
1036 renderer='string_html', xhr=True)
1036 def pullrequest_todos(self):
1037 def pullrequest_todos(self):
1037 self.load_default_context()
1038 self.load_default_context()
1038
1039
1039 pull_request = PullRequest.get_or_404(
1040 pull_request = PullRequest.get_or_404(
1040 self.request.matchdict['pull_request_id'])
1041 self.request.matchdict['pull_request_id'])
1041 pull_request_id = pull_request.pull_request_id
1042 pull_request_id = pull_request.pull_request_id
1042 version = self.request.GET.get('version')
1043 version = self.request.GET.get('version')
1043
1044
1044 _render = self.request.get_partial_renderer(
1045 _render = self.request.get_partial_renderer(
1045 'rhodecode:templates/base/sidebar.mako')
1046 'rhodecode:templates/base/sidebar.mako')
1046 c = _render.get_call_context()
1047 c = _render.get_call_context()
1047 (pull_request_latest,
1048 (pull_request_latest,
1048 pull_request_at_ver,
1049 pull_request_at_ver,
1049 pull_request_display_obj,
1050 pull_request_display_obj,
1050 at_version) = PullRequestModel().get_pr_version(
1051 at_version) = PullRequestModel().get_pr_version(
1051 pull_request_id, version=version)
1052 pull_request_id, version=version)
1052 versions = pull_request_display_obj.versions()
1053 versions = pull_request_display_obj.versions()
1053 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1054 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1054 c.versions = versions + [latest_ver]
1055 c.versions = versions + [latest_ver]
1055
1056
1056 c.at_version = at_version
1057 c.at_version = at_version
1057 c.at_version_num = (at_version
1058 c.at_version_num = (at_version
1058 if at_version and at_version != PullRequest.LATEST_VER
1059 if at_version and at_version != PullRequest.LATEST_VER
1059 else None)
1060 else None)
1060
1061
1061 c.unresolved_comments = CommentsModel() \
1062 c.unresolved_comments = CommentsModel() \
1062 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1063 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1063 c.resolved_comments = CommentsModel() \
1064 c.resolved_comments = CommentsModel() \
1064 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1065 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1065
1066
1066 all_comments = c.unresolved_comments + c.resolved_comments
1067 all_comments = c.unresolved_comments + c.resolved_comments
1067 existing_ids = self._get_existing_ids(self.request.POST)
1068 existing_ids = self.get_comment_ids(self.request.POST)
1068 return _render('comments_table', all_comments, len(c.unresolved_comments),
1069 return _render('comments_table', all_comments, len(c.unresolved_comments),
1069 todo_comments=True, existing_ids=existing_ids)
1070 todo_comments=True, existing_ids=existing_ids)
1070
1071
1071 @LoginRequired()
1072 @LoginRequired()
1072 @NotAnonymous()
1073 @NotAnonymous()
1073 @HasRepoPermissionAnyDecorator(
1074 @HasRepoPermissionAnyDecorator(
1074 'repository.read', 'repository.write', 'repository.admin')
1075 'repository.read', 'repository.write', 'repository.admin')
1075 @CSRFRequired()
1076 @CSRFRequired()
1076 @view_config(
1077 @view_config(
1077 route_name='pullrequest_create', request_method='POST',
1078 route_name='pullrequest_create', request_method='POST',
1078 renderer=None)
1079 renderer=None)
1079 def pull_request_create(self):
1080 def pull_request_create(self):
1080 _ = self.request.translate
1081 _ = self.request.translate
1081 self.assure_not_empty_repo()
1082 self.assure_not_empty_repo()
1082 self.load_default_context()
1083 self.load_default_context()
1083
1084
1084 controls = peppercorn.parse(self.request.POST.items())
1085 controls = peppercorn.parse(self.request.POST.items())
1085
1086
1086 try:
1087 try:
1087 form = PullRequestForm(
1088 form = PullRequestForm(
1088 self.request.translate, self.db_repo.repo_id)()
1089 self.request.translate, self.db_repo.repo_id)()
1089 _form = form.to_python(controls)
1090 _form = form.to_python(controls)
1090 except formencode.Invalid as errors:
1091 except formencode.Invalid as errors:
1091 if errors.error_dict.get('revisions'):
1092 if errors.error_dict.get('revisions'):
1092 msg = 'Revisions: %s' % errors.error_dict['revisions']
1093 msg = 'Revisions: %s' % errors.error_dict['revisions']
1093 elif errors.error_dict.get('pullrequest_title'):
1094 elif errors.error_dict.get('pullrequest_title'):
1094 msg = errors.error_dict.get('pullrequest_title')
1095 msg = errors.error_dict.get('pullrequest_title')
1095 else:
1096 else:
1096 msg = _('Error creating pull request: {}').format(errors)
1097 msg = _('Error creating pull request: {}').format(errors)
1097 log.exception(msg)
1098 log.exception(msg)
1098 h.flash(msg, 'error')
1099 h.flash(msg, 'error')
1099
1100
1100 # would rather just go back to form ...
1101 # would rather just go back to form ...
1101 raise HTTPFound(
1102 raise HTTPFound(
1102 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1103 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1103
1104
1104 source_repo = _form['source_repo']
1105 source_repo = _form['source_repo']
1105 source_ref = _form['source_ref']
1106 source_ref = _form['source_ref']
1106 target_repo = _form['target_repo']
1107 target_repo = _form['target_repo']
1107 target_ref = _form['target_ref']
1108 target_ref = _form['target_ref']
1108 commit_ids = _form['revisions'][::-1]
1109 commit_ids = _form['revisions'][::-1]
1109 common_ancestor_id = _form['common_ancestor']
1110 common_ancestor_id = _form['common_ancestor']
1110
1111
1111 # find the ancestor for this pr
1112 # find the ancestor for this pr
1112 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1113 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1113 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1114 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1114
1115
1115 if not (source_db_repo or target_db_repo):
1116 if not (source_db_repo or target_db_repo):
1116 h.flash(_('source_repo or target repo not found'), category='error')
1117 h.flash(_('source_repo or target repo not found'), category='error')
1117 raise HTTPFound(
1118 raise HTTPFound(
1118 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1119 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1119
1120
1120 # re-check permissions again here
1121 # re-check permissions again here
1121 # source_repo we must have read permissions
1122 # source_repo we must have read permissions
1122
1123
1123 source_perm = HasRepoPermissionAny(
1124 source_perm = HasRepoPermissionAny(
1124 'repository.read', 'repository.write', 'repository.admin')(
1125 'repository.read', 'repository.write', 'repository.admin')(
1125 source_db_repo.repo_name)
1126 source_db_repo.repo_name)
1126 if not source_perm:
1127 if not source_perm:
1127 msg = _('Not Enough permissions to source repo `{}`.'.format(
1128 msg = _('Not Enough permissions to source repo `{}`.'.format(
1128 source_db_repo.repo_name))
1129 source_db_repo.repo_name))
1129 h.flash(msg, category='error')
1130 h.flash(msg, category='error')
1130 # copy the args back to redirect
1131 # copy the args back to redirect
1131 org_query = self.request.GET.mixed()
1132 org_query = self.request.GET.mixed()
1132 raise HTTPFound(
1133 raise HTTPFound(
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1134 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1134 _query=org_query))
1135 _query=org_query))
1135
1136
1136 # target repo we must have read permissions, and also later on
1137 # target repo we must have read permissions, and also later on
1137 # we want to check branch permissions here
1138 # we want to check branch permissions here
1138 target_perm = HasRepoPermissionAny(
1139 target_perm = HasRepoPermissionAny(
1139 'repository.read', 'repository.write', 'repository.admin')(
1140 'repository.read', 'repository.write', 'repository.admin')(
1140 target_db_repo.repo_name)
1141 target_db_repo.repo_name)
1141 if not target_perm:
1142 if not target_perm:
1142 msg = _('Not Enough permissions to target repo `{}`.'.format(
1143 msg = _('Not Enough permissions to target repo `{}`.'.format(
1143 target_db_repo.repo_name))
1144 target_db_repo.repo_name))
1144 h.flash(msg, category='error')
1145 h.flash(msg, category='error')
1145 # copy the args back to redirect
1146 # copy the args back to redirect
1146 org_query = self.request.GET.mixed()
1147 org_query = self.request.GET.mixed()
1147 raise HTTPFound(
1148 raise HTTPFound(
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1149 _query=org_query))
1150 _query=org_query))
1150
1151
1151 source_scm = source_db_repo.scm_instance()
1152 source_scm = source_db_repo.scm_instance()
1152 target_scm = target_db_repo.scm_instance()
1153 target_scm = target_db_repo.scm_instance()
1153
1154
1154 source_ref_obj = unicode_to_reference(source_ref)
1155 source_ref_obj = unicode_to_reference(source_ref)
1155 target_ref_obj = unicode_to_reference(target_ref)
1156 target_ref_obj = unicode_to_reference(target_ref)
1156
1157
1157 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1158 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1158 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1159 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1159
1160
1160 ancestor = source_scm.get_common_ancestor(
1161 ancestor = source_scm.get_common_ancestor(
1161 source_commit.raw_id, target_commit.raw_id, target_scm)
1162 source_commit.raw_id, target_commit.raw_id, target_scm)
1162
1163
1163 # recalculate target ref based on ancestor
1164 # recalculate target ref based on ancestor
1164 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1165 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1165
1166
1166 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1167 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1167 PullRequestModel().get_reviewer_functions()
1168 PullRequestModel().get_reviewer_functions()
1168
1169
1169 # recalculate reviewers logic, to make sure we can validate this
1170 # recalculate reviewers logic, to make sure we can validate this
1170 reviewer_rules = get_default_reviewers_data(
1171 reviewer_rules = get_default_reviewers_data(
1171 self._rhodecode_db_user,
1172 self._rhodecode_db_user,
1172 source_db_repo,
1173 source_db_repo,
1173 source_ref_obj,
1174 source_ref_obj,
1174 target_db_repo,
1175 target_db_repo,
1175 target_ref_obj,
1176 target_ref_obj,
1176 include_diff_info=False)
1177 include_diff_info=False)
1177
1178
1178 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1179 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1179 observers = validate_observers(_form['observer_members'], reviewer_rules)
1180 observers = validate_observers(_form['observer_members'], reviewer_rules)
1180
1181
1181 pullrequest_title = _form['pullrequest_title']
1182 pullrequest_title = _form['pullrequest_title']
1182 title_source_ref = source_ref_obj.name
1183 title_source_ref = source_ref_obj.name
1183 if not pullrequest_title:
1184 if not pullrequest_title:
1184 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1185 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1185 source=source_repo,
1186 source=source_repo,
1186 source_ref=title_source_ref,
1187 source_ref=title_source_ref,
1187 target=target_repo
1188 target=target_repo
1188 )
1189 )
1189
1190
1190 description = _form['pullrequest_desc']
1191 description = _form['pullrequest_desc']
1191 description_renderer = _form['description_renderer']
1192 description_renderer = _form['description_renderer']
1192
1193
1193 try:
1194 try:
1194 pull_request = PullRequestModel().create(
1195 pull_request = PullRequestModel().create(
1195 created_by=self._rhodecode_user.user_id,
1196 created_by=self._rhodecode_user.user_id,
1196 source_repo=source_repo,
1197 source_repo=source_repo,
1197 source_ref=source_ref,
1198 source_ref=source_ref,
1198 target_repo=target_repo,
1199 target_repo=target_repo,
1199 target_ref=target_ref,
1200 target_ref=target_ref,
1200 revisions=commit_ids,
1201 revisions=commit_ids,
1201 common_ancestor_id=common_ancestor_id,
1202 common_ancestor_id=common_ancestor_id,
1202 reviewers=reviewers,
1203 reviewers=reviewers,
1203 observers=observers,
1204 observers=observers,
1204 title=pullrequest_title,
1205 title=pullrequest_title,
1205 description=description,
1206 description=description,
1206 description_renderer=description_renderer,
1207 description_renderer=description_renderer,
1207 reviewer_data=reviewer_rules,
1208 reviewer_data=reviewer_rules,
1208 auth_user=self._rhodecode_user
1209 auth_user=self._rhodecode_user
1209 )
1210 )
1210 Session().commit()
1211 Session().commit()
1211
1212
1212 h.flash(_('Successfully opened new pull request'),
1213 h.flash(_('Successfully opened new pull request'),
1213 category='success')
1214 category='success')
1214 except Exception:
1215 except Exception:
1215 msg = _('Error occurred during creation of this pull request.')
1216 msg = _('Error occurred during creation of this pull request.')
1216 log.exception(msg)
1217 log.exception(msg)
1217 h.flash(msg, category='error')
1218 h.flash(msg, category='error')
1218
1219
1219 # copy the args back to redirect
1220 # copy the args back to redirect
1220 org_query = self.request.GET.mixed()
1221 org_query = self.request.GET.mixed()
1221 raise HTTPFound(
1222 raise HTTPFound(
1222 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1223 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1223 _query=org_query))
1224 _query=org_query))
1224
1225
1225 raise HTTPFound(
1226 raise HTTPFound(
1226 h.route_path('pullrequest_show', repo_name=target_repo,
1227 h.route_path('pullrequest_show', repo_name=target_repo,
1227 pull_request_id=pull_request.pull_request_id))
1228 pull_request_id=pull_request.pull_request_id))
1228
1229
1229 @LoginRequired()
1230 @LoginRequired()
1230 @NotAnonymous()
1231 @NotAnonymous()
1231 @HasRepoPermissionAnyDecorator(
1232 @HasRepoPermissionAnyDecorator(
1232 'repository.read', 'repository.write', 'repository.admin')
1233 'repository.read', 'repository.write', 'repository.admin')
1233 @CSRFRequired()
1234 @CSRFRequired()
1234 @view_config(
1235 @view_config(
1235 route_name='pullrequest_update', request_method='POST',
1236 route_name='pullrequest_update', request_method='POST',
1236 renderer='json_ext')
1237 renderer='json_ext')
1237 def pull_request_update(self):
1238 def pull_request_update(self):
1238 pull_request = PullRequest.get_or_404(
1239 pull_request = PullRequest.get_or_404(
1239 self.request.matchdict['pull_request_id'])
1240 self.request.matchdict['pull_request_id'])
1240 _ = self.request.translate
1241 _ = self.request.translate
1241
1242
1242 c = self.load_default_context()
1243 c = self.load_default_context()
1243 redirect_url = None
1244 redirect_url = None
1244
1245
1245 if pull_request.is_closed():
1246 if pull_request.is_closed():
1246 log.debug('update: forbidden because pull request is closed')
1247 log.debug('update: forbidden because pull request is closed')
1247 msg = _(u'Cannot update closed pull requests.')
1248 msg = _(u'Cannot update closed pull requests.')
1248 h.flash(msg, category='error')
1249 h.flash(msg, category='error')
1249 return {'response': True,
1250 return {'response': True,
1250 'redirect_url': redirect_url}
1251 'redirect_url': redirect_url}
1251
1252
1252 is_state_changing = pull_request.is_state_changing()
1253 is_state_changing = pull_request.is_state_changing()
1253 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1254 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1254
1255
1255 # only owner or admin can update it
1256 # only owner or admin can update it
1256 allowed_to_update = PullRequestModel().check_user_update(
1257 allowed_to_update = PullRequestModel().check_user_update(
1257 pull_request, self._rhodecode_user)
1258 pull_request, self._rhodecode_user)
1258
1259
1259 if allowed_to_update:
1260 if allowed_to_update:
1260 controls = peppercorn.parse(self.request.POST.items())
1261 controls = peppercorn.parse(self.request.POST.items())
1261 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1262 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1262
1263
1263 if 'review_members' in controls:
1264 if 'review_members' in controls:
1264 self._update_reviewers(
1265 self._update_reviewers(
1265 c,
1266 c,
1266 pull_request, controls['review_members'],
1267 pull_request, controls['review_members'],
1267 pull_request.reviewer_data,
1268 pull_request.reviewer_data,
1268 PullRequestReviewers.ROLE_REVIEWER)
1269 PullRequestReviewers.ROLE_REVIEWER)
1269 elif 'observer_members' in controls:
1270 elif 'observer_members' in controls:
1270 self._update_reviewers(
1271 self._update_reviewers(
1271 c,
1272 c,
1272 pull_request, controls['observer_members'],
1273 pull_request, controls['observer_members'],
1273 pull_request.reviewer_data,
1274 pull_request.reviewer_data,
1274 PullRequestReviewers.ROLE_OBSERVER)
1275 PullRequestReviewers.ROLE_OBSERVER)
1275 elif str2bool(self.request.POST.get('update_commits', 'false')):
1276 elif str2bool(self.request.POST.get('update_commits', 'false')):
1276 if is_state_changing:
1277 if is_state_changing:
1277 log.debug('commits update: forbidden because pull request is in state %s',
1278 log.debug('commits update: forbidden because pull request is in state %s',
1278 pull_request.pull_request_state)
1279 pull_request.pull_request_state)
1279 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1280 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1280 u'Current state is: `{}`').format(
1281 u'Current state is: `{}`').format(
1281 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1282 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1282 h.flash(msg, category='error')
1283 h.flash(msg, category='error')
1283 return {'response': True,
1284 return {'response': True,
1284 'redirect_url': redirect_url}
1285 'redirect_url': redirect_url}
1285
1286
1286 self._update_commits(c, pull_request)
1287 self._update_commits(c, pull_request)
1287 if force_refresh:
1288 if force_refresh:
1288 redirect_url = h.route_path(
1289 redirect_url = h.route_path(
1289 'pullrequest_show', repo_name=self.db_repo_name,
1290 'pullrequest_show', repo_name=self.db_repo_name,
1290 pull_request_id=pull_request.pull_request_id,
1291 pull_request_id=pull_request.pull_request_id,
1291 _query={"force_refresh": 1})
1292 _query={"force_refresh": 1})
1292 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1293 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1293 self._edit_pull_request(pull_request)
1294 self._edit_pull_request(pull_request)
1294 else:
1295 else:
1295 log.error('Unhandled update data.')
1296 log.error('Unhandled update data.')
1296 raise HTTPBadRequest()
1297 raise HTTPBadRequest()
1297
1298
1298 return {'response': True,
1299 return {'response': True,
1299 'redirect_url': redirect_url}
1300 'redirect_url': redirect_url}
1300 raise HTTPForbidden()
1301 raise HTTPForbidden()
1301
1302
1302 def _edit_pull_request(self, pull_request):
1303 def _edit_pull_request(self, pull_request):
1303 """
1304 """
1304 Edit title and description
1305 Edit title and description
1305 """
1306 """
1306 _ = self.request.translate
1307 _ = self.request.translate
1307
1308
1308 try:
1309 try:
1309 PullRequestModel().edit(
1310 PullRequestModel().edit(
1310 pull_request,
1311 pull_request,
1311 self.request.POST.get('title'),
1312 self.request.POST.get('title'),
1312 self.request.POST.get('description'),
1313 self.request.POST.get('description'),
1313 self.request.POST.get('description_renderer'),
1314 self.request.POST.get('description_renderer'),
1314 self._rhodecode_user)
1315 self._rhodecode_user)
1315 except ValueError:
1316 except ValueError:
1316 msg = _(u'Cannot update closed pull requests.')
1317 msg = _(u'Cannot update closed pull requests.')
1317 h.flash(msg, category='error')
1318 h.flash(msg, category='error')
1318 return
1319 return
1319 else:
1320 else:
1320 Session().commit()
1321 Session().commit()
1321
1322
1322 msg = _(u'Pull request title & description updated.')
1323 msg = _(u'Pull request title & description updated.')
1323 h.flash(msg, category='success')
1324 h.flash(msg, category='success')
1324 return
1325 return
1325
1326
1326 def _update_commits(self, c, pull_request):
1327 def _update_commits(self, c, pull_request):
1327 _ = self.request.translate
1328 _ = self.request.translate
1328
1329
1329 with pull_request.set_state(PullRequest.STATE_UPDATING):
1330 with pull_request.set_state(PullRequest.STATE_UPDATING):
1330 resp = PullRequestModel().update_commits(
1331 resp = PullRequestModel().update_commits(
1331 pull_request, self._rhodecode_db_user)
1332 pull_request, self._rhodecode_db_user)
1332
1333
1333 if resp.executed:
1334 if resp.executed:
1334
1335
1335 if resp.target_changed and resp.source_changed:
1336 if resp.target_changed and resp.source_changed:
1336 changed = 'target and source repositories'
1337 changed = 'target and source repositories'
1337 elif resp.target_changed and not resp.source_changed:
1338 elif resp.target_changed and not resp.source_changed:
1338 changed = 'target repository'
1339 changed = 'target repository'
1339 elif not resp.target_changed and resp.source_changed:
1340 elif not resp.target_changed and resp.source_changed:
1340 changed = 'source repository'
1341 changed = 'source repository'
1341 else:
1342 else:
1342 changed = 'nothing'
1343 changed = 'nothing'
1343
1344
1344 msg = _(u'Pull request updated to "{source_commit_id}" with '
1345 msg = _(u'Pull request updated to "{source_commit_id}" with '
1345 u'{count_added} added, {count_removed} removed commits. '
1346 u'{count_added} added, {count_removed} removed commits. '
1346 u'Source of changes: {change_source}.')
1347 u'Source of changes: {change_source}.')
1347 msg = msg.format(
1348 msg = msg.format(
1348 source_commit_id=pull_request.source_ref_parts.commit_id,
1349 source_commit_id=pull_request.source_ref_parts.commit_id,
1349 count_added=len(resp.changes.added),
1350 count_added=len(resp.changes.added),
1350 count_removed=len(resp.changes.removed),
1351 count_removed=len(resp.changes.removed),
1351 change_source=changed)
1352 change_source=changed)
1352 h.flash(msg, category='success')
1353 h.flash(msg, category='success')
1353 channelstream.pr_update_channelstream_push(
1354 channelstream.pr_update_channelstream_push(
1354 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1355 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1355 else:
1356 else:
1356 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1357 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1357 warning_reasons = [
1358 warning_reasons = [
1358 UpdateFailureReason.NO_CHANGE,
1359 UpdateFailureReason.NO_CHANGE,
1359 UpdateFailureReason.WRONG_REF_TYPE,
1360 UpdateFailureReason.WRONG_REF_TYPE,
1360 ]
1361 ]
1361 category = 'warning' if resp.reason in warning_reasons else 'error'
1362 category = 'warning' if resp.reason in warning_reasons else 'error'
1362 h.flash(msg, category=category)
1363 h.flash(msg, category=category)
1363
1364
1364 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1365 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1365 _ = self.request.translate
1366 _ = self.request.translate
1366
1367
1367 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1368 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1368 PullRequestModel().get_reviewer_functions()
1369 PullRequestModel().get_reviewer_functions()
1369
1370
1370 if role == PullRequestReviewers.ROLE_REVIEWER:
1371 if role == PullRequestReviewers.ROLE_REVIEWER:
1371 try:
1372 try:
1372 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1373 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1373 except ValueError as e:
1374 except ValueError as e:
1374 log.error('Reviewers Validation: {}'.format(e))
1375 log.error('Reviewers Validation: {}'.format(e))
1375 h.flash(e, category='error')
1376 h.flash(e, category='error')
1376 return
1377 return
1377
1378
1378 old_calculated_status = pull_request.calculated_review_status()
1379 old_calculated_status = pull_request.calculated_review_status()
1379 PullRequestModel().update_reviewers(
1380 PullRequestModel().update_reviewers(
1380 pull_request, reviewers, self._rhodecode_db_user)
1381 pull_request, reviewers, self._rhodecode_db_user)
1381
1382
1382 Session().commit()
1383 Session().commit()
1383
1384
1384 msg = _('Pull request reviewers updated.')
1385 msg = _('Pull request reviewers updated.')
1385 h.flash(msg, category='success')
1386 h.flash(msg, category='success')
1386 channelstream.pr_update_channelstream_push(
1387 channelstream.pr_update_channelstream_push(
1387 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1388 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1388
1389
1389 # trigger status changed if change in reviewers changes the status
1390 # trigger status changed if change in reviewers changes the status
1390 calculated_status = pull_request.calculated_review_status()
1391 calculated_status = pull_request.calculated_review_status()
1391 if old_calculated_status != calculated_status:
1392 if old_calculated_status != calculated_status:
1392 PullRequestModel().trigger_pull_request_hook(
1393 PullRequestModel().trigger_pull_request_hook(
1393 pull_request, self._rhodecode_user, 'review_status_change',
1394 pull_request, self._rhodecode_user, 'review_status_change',
1394 data={'status': calculated_status})
1395 data={'status': calculated_status})
1395
1396
1396 elif role == PullRequestReviewers.ROLE_OBSERVER:
1397 elif role == PullRequestReviewers.ROLE_OBSERVER:
1397 try:
1398 try:
1398 observers = validate_observers(review_members, reviewer_rules)
1399 observers = validate_observers(review_members, reviewer_rules)
1399 except ValueError as e:
1400 except ValueError as e:
1400 log.error('Observers Validation: {}'.format(e))
1401 log.error('Observers Validation: {}'.format(e))
1401 h.flash(e, category='error')
1402 h.flash(e, category='error')
1402 return
1403 return
1403
1404
1404 PullRequestModel().update_observers(
1405 PullRequestModel().update_observers(
1405 pull_request, observers, self._rhodecode_db_user)
1406 pull_request, observers, self._rhodecode_db_user)
1406
1407
1407 Session().commit()
1408 Session().commit()
1408 msg = _('Pull request observers updated.')
1409 msg = _('Pull request observers updated.')
1409 h.flash(msg, category='success')
1410 h.flash(msg, category='success')
1410 channelstream.pr_update_channelstream_push(
1411 channelstream.pr_update_channelstream_push(
1411 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1412 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1412
1413
1413 @LoginRequired()
1414 @LoginRequired()
1414 @NotAnonymous()
1415 @NotAnonymous()
1415 @HasRepoPermissionAnyDecorator(
1416 @HasRepoPermissionAnyDecorator(
1416 'repository.read', 'repository.write', 'repository.admin')
1417 'repository.read', 'repository.write', 'repository.admin')
1417 @CSRFRequired()
1418 @CSRFRequired()
1418 @view_config(
1419 @view_config(
1419 route_name='pullrequest_merge', request_method='POST',
1420 route_name='pullrequest_merge', request_method='POST',
1420 renderer='json_ext')
1421 renderer='json_ext')
1421 def pull_request_merge(self):
1422 def pull_request_merge(self):
1422 """
1423 """
1423 Merge will perform a server-side merge of the specified
1424 Merge will perform a server-side merge of the specified
1424 pull request, if the pull request is approved and mergeable.
1425 pull request, if the pull request is approved and mergeable.
1425 After successful merging, the pull request is automatically
1426 After successful merging, the pull request is automatically
1426 closed, with a relevant comment.
1427 closed, with a relevant comment.
1427 """
1428 """
1428 pull_request = PullRequest.get_or_404(
1429 pull_request = PullRequest.get_or_404(
1429 self.request.matchdict['pull_request_id'])
1430 self.request.matchdict['pull_request_id'])
1430 _ = self.request.translate
1431 _ = self.request.translate
1431
1432
1432 if pull_request.is_state_changing():
1433 if pull_request.is_state_changing():
1433 log.debug('show: forbidden because pull request is in state %s',
1434 log.debug('show: forbidden because pull request is in state %s',
1434 pull_request.pull_request_state)
1435 pull_request.pull_request_state)
1435 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1436 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1436 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1437 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1437 pull_request.pull_request_state)
1438 pull_request.pull_request_state)
1438 h.flash(msg, category='error')
1439 h.flash(msg, category='error')
1439 raise HTTPFound(
1440 raise HTTPFound(
1440 h.route_path('pullrequest_show',
1441 h.route_path('pullrequest_show',
1441 repo_name=pull_request.target_repo.repo_name,
1442 repo_name=pull_request.target_repo.repo_name,
1442 pull_request_id=pull_request.pull_request_id))
1443 pull_request_id=pull_request.pull_request_id))
1443
1444
1444 self.load_default_context()
1445 self.load_default_context()
1445
1446
1446 with pull_request.set_state(PullRequest.STATE_UPDATING):
1447 with pull_request.set_state(PullRequest.STATE_UPDATING):
1447 check = MergeCheck.validate(
1448 check = MergeCheck.validate(
1448 pull_request, auth_user=self._rhodecode_user,
1449 pull_request, auth_user=self._rhodecode_user,
1449 translator=self.request.translate)
1450 translator=self.request.translate)
1450 merge_possible = not check.failed
1451 merge_possible = not check.failed
1451
1452
1452 for err_type, error_msg in check.errors:
1453 for err_type, error_msg in check.errors:
1453 h.flash(error_msg, category=err_type)
1454 h.flash(error_msg, category=err_type)
1454
1455
1455 if merge_possible:
1456 if merge_possible:
1456 log.debug("Pre-conditions checked, trying to merge.")
1457 log.debug("Pre-conditions checked, trying to merge.")
1457 extras = vcs_operation_context(
1458 extras = vcs_operation_context(
1458 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1459 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1459 username=self._rhodecode_db_user.username, action='push',
1460 username=self._rhodecode_db_user.username, action='push',
1460 scm=pull_request.target_repo.repo_type)
1461 scm=pull_request.target_repo.repo_type)
1461 with pull_request.set_state(PullRequest.STATE_UPDATING):
1462 with pull_request.set_state(PullRequest.STATE_UPDATING):
1462 self._merge_pull_request(
1463 self._merge_pull_request(
1463 pull_request, self._rhodecode_db_user, extras)
1464 pull_request, self._rhodecode_db_user, extras)
1464 else:
1465 else:
1465 log.debug("Pre-conditions failed, NOT merging.")
1466 log.debug("Pre-conditions failed, NOT merging.")
1466
1467
1467 raise HTTPFound(
1468 raise HTTPFound(
1468 h.route_path('pullrequest_show',
1469 h.route_path('pullrequest_show',
1469 repo_name=pull_request.target_repo.repo_name,
1470 repo_name=pull_request.target_repo.repo_name,
1470 pull_request_id=pull_request.pull_request_id))
1471 pull_request_id=pull_request.pull_request_id))
1471
1472
1472 def _merge_pull_request(self, pull_request, user, extras):
1473 def _merge_pull_request(self, pull_request, user, extras):
1473 _ = self.request.translate
1474 _ = self.request.translate
1474 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1475 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1475
1476
1476 if merge_resp.executed:
1477 if merge_resp.executed:
1477 log.debug("The merge was successful, closing the pull request.")
1478 log.debug("The merge was successful, closing the pull request.")
1478 PullRequestModel().close_pull_request(
1479 PullRequestModel().close_pull_request(
1479 pull_request.pull_request_id, user)
1480 pull_request.pull_request_id, user)
1480 Session().commit()
1481 Session().commit()
1481 msg = _('Pull request was successfully merged and closed.')
1482 msg = _('Pull request was successfully merged and closed.')
1482 h.flash(msg, category='success')
1483 h.flash(msg, category='success')
1483 else:
1484 else:
1484 log.debug(
1485 log.debug(
1485 "The merge was not successful. Merge response: %s", merge_resp)
1486 "The merge was not successful. Merge response: %s", merge_resp)
1486 msg = merge_resp.merge_status_message
1487 msg = merge_resp.merge_status_message
1487 h.flash(msg, category='error')
1488 h.flash(msg, category='error')
1488
1489
1489 @LoginRequired()
1490 @LoginRequired()
1490 @NotAnonymous()
1491 @NotAnonymous()
1491 @HasRepoPermissionAnyDecorator(
1492 @HasRepoPermissionAnyDecorator(
1492 'repository.read', 'repository.write', 'repository.admin')
1493 'repository.read', 'repository.write', 'repository.admin')
1493 @CSRFRequired()
1494 @CSRFRequired()
1494 @view_config(
1495 @view_config(
1495 route_name='pullrequest_delete', request_method='POST',
1496 route_name='pullrequest_delete', request_method='POST',
1496 renderer='json_ext')
1497 renderer='json_ext')
1497 def pull_request_delete(self):
1498 def pull_request_delete(self):
1498 _ = self.request.translate
1499 _ = self.request.translate
1499
1500
1500 pull_request = PullRequest.get_or_404(
1501 pull_request = PullRequest.get_or_404(
1501 self.request.matchdict['pull_request_id'])
1502 self.request.matchdict['pull_request_id'])
1502 self.load_default_context()
1503 self.load_default_context()
1503
1504
1504 pr_closed = pull_request.is_closed()
1505 pr_closed = pull_request.is_closed()
1505 allowed_to_delete = PullRequestModel().check_user_delete(
1506 allowed_to_delete = PullRequestModel().check_user_delete(
1506 pull_request, self._rhodecode_user) and not pr_closed
1507 pull_request, self._rhodecode_user) and not pr_closed
1507
1508
1508 # only owner can delete it !
1509 # only owner can delete it !
1509 if allowed_to_delete:
1510 if allowed_to_delete:
1510 PullRequestModel().delete(pull_request, self._rhodecode_user)
1511 PullRequestModel().delete(pull_request, self._rhodecode_user)
1511 Session().commit()
1512 Session().commit()
1512 h.flash(_('Successfully deleted pull request'),
1513 h.flash(_('Successfully deleted pull request'),
1513 category='success')
1514 category='success')
1514 raise HTTPFound(h.route_path('pullrequest_show_all',
1515 raise HTTPFound(h.route_path('pullrequest_show_all',
1515 repo_name=self.db_repo_name))
1516 repo_name=self.db_repo_name))
1516
1517
1517 log.warning('user %s tried to delete pull request without access',
1518 log.warning('user %s tried to delete pull request without access',
1518 self._rhodecode_user)
1519 self._rhodecode_user)
1519 raise HTTPNotFound()
1520 raise HTTPNotFound()
1520
1521
1521 @LoginRequired()
1522 def _pull_request_comments_create(self, pull_request, comments):
1522 @NotAnonymous()
1523 @HasRepoPermissionAnyDecorator(
1524 'repository.read', 'repository.write', 'repository.admin')
1525 @CSRFRequired()
1526 @view_config(
1527 route_name='pullrequest_comment_create', request_method='POST',
1528 renderer='json_ext')
1529 def pull_request_comment_create(self):
1530 _ = self.request.translate
1523 _ = self.request.translate
1524 data = {}
1525 pull_request_id = pull_request.pull_request_id
1526 if not comments:
1527 return
1531
1528
1532 pull_request = PullRequest.get_or_404(
1529 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1533 self.request.matchdict['pull_request_id'])
1534 pull_request_id = pull_request.pull_request_id
1535
1530
1536 if pull_request.is_closed():
1531 for entry in comments:
1537 log.debug('comment: forbidden because pull request is closed')
1538 raise HTTPForbidden()
1539
1540 allowed_to_comment = PullRequestModel().check_user_comment(
1541 pull_request, self._rhodecode_user)
1542 if not allowed_to_comment:
1543 log.debug('comment: forbidden because pull request is from forbidden repo')
1544 raise HTTPForbidden()
1545
1546 c = self.load_default_context()
1532 c = self.load_default_context()
1547
1533 comment_type = entry['comment_type']
1548 status = self.request.POST.get('changeset_status', None)
1534 text = entry['text']
1549 text = self.request.POST.get('text')
1535 status = entry['status']
1550 comment_type = self.request.POST.get('comment_type')
1536 is_draft = str2bool(entry['is_draft'])
1551 is_draft = str2bool(self.request.POST.get('draft'))
1537 resolves_comment_id = entry['resolves_comment_id']
1552 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1538 close_pull_request = entry['close_pull_request']
1553 close_pull_request = self.request.POST.get('close_pull_request')
1539 f_path = entry['f_path']
1540 line_no = entry['line']
1541 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1554
1542
1555 # the logic here should work like following, if we submit close
1543 # the logic here should work like following, if we submit close
1556 # pr comment, use `close_pull_request_with_comment` function
1544 # pr comment, use `close_pull_request_with_comment` function
1557 # else handle regular comment logic
1545 # else handle regular comment logic
1558
1546
1559 if close_pull_request:
1547 if close_pull_request:
1560 # only owner or admin or person with write permissions
1548 # only owner or admin or person with write permissions
1561 allowed_to_close = PullRequestModel().check_user_update(
1549 allowed_to_close = PullRequestModel().check_user_update(
1562 pull_request, self._rhodecode_user)
1550 pull_request, self._rhodecode_user)
1563 if not allowed_to_close:
1551 if not allowed_to_close:
1564 log.debug('comment: forbidden because not allowed to close '
1552 log.debug('comment: forbidden because not allowed to close '
1565 'pull request %s', pull_request_id)
1553 'pull request %s', pull_request_id)
1566 raise HTTPForbidden()
1554 raise HTTPForbidden()
1567
1555
1568 # This also triggers `review_status_change`
1556 # This also triggers `review_status_change`
1569 comment, status = PullRequestModel().close_pull_request_with_comment(
1557 comment, status = PullRequestModel().close_pull_request_with_comment(
1570 pull_request, self._rhodecode_user, self.db_repo, message=text,
1558 pull_request, self._rhodecode_user, self.db_repo, message=text,
1571 auth_user=self._rhodecode_user)
1559 auth_user=self._rhodecode_user)
1572 Session().flush()
1560 Session().flush()
1573 is_inline = comment.is_inline
1561 is_inline = comment.is_inline
1574
1562
1575 PullRequestModel().trigger_pull_request_hook(
1563 PullRequestModel().trigger_pull_request_hook(
1576 pull_request, self._rhodecode_user, 'comment',
1564 pull_request, self._rhodecode_user, 'comment',
1577 data={'comment': comment})
1565 data={'comment': comment})
1578
1566
1579 else:
1567 else:
1580 # regular comment case, could be inline, or one with status.
1568 # regular comment case, could be inline, or one with status.
1581 # for that one we check also permissions
1569 # for that one we check also permissions
1582 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1570 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1583 allowed_to_change_status = PullRequestModel().check_user_change_status(
1571 allowed_to_change_status = PullRequestModel().check_user_change_status(
1584 pull_request, self._rhodecode_user) and not is_draft
1572 pull_request, self._rhodecode_user) and not is_draft
1585
1573
1586 if status and allowed_to_change_status:
1574 if status and allowed_to_change_status:
1587 message = (_('Status change %(transition_icon)s %(status)s')
1575 message = (_('Status change %(transition_icon)s %(status)s')
1588 % {'transition_icon': '>',
1576 % {'transition_icon': '>',
1589 'status': ChangesetStatus.get_status_lbl(status)})
1577 'status': ChangesetStatus.get_status_lbl(status)})
1590 text = text or message
1578 text = text or message
1591
1579
1592 comment = CommentsModel().create(
1580 comment = CommentsModel().create(
1593 text=text,
1581 text=text,
1594 repo=self.db_repo.repo_id,
1582 repo=self.db_repo.repo_id,
1595 user=self._rhodecode_user.user_id,
1583 user=self._rhodecode_user.user_id,
1596 pull_request=pull_request,
1584 pull_request=pull_request,
1597 f_path=self.request.POST.get('f_path'),
1585 f_path=f_path,
1598 line_no=self.request.POST.get('line'),
1586 line_no=line_no,
1599 status_change=(ChangesetStatus.get_status_lbl(status)
1587 status_change=(ChangesetStatus.get_status_lbl(status)
1600 if status and allowed_to_change_status else None),
1588 if status and allowed_to_change_status else None),
1601 status_change_type=(status
1589 status_change_type=(status
1602 if status and allowed_to_change_status else None),
1590 if status and allowed_to_change_status else None),
1603 comment_type=comment_type,
1591 comment_type=comment_type,
1604 is_draft=is_draft,
1592 is_draft=is_draft,
1605 resolves_comment_id=resolves_comment_id,
1593 resolves_comment_id=resolves_comment_id,
1606 auth_user=self._rhodecode_user,
1594 auth_user=self._rhodecode_user,
1607 send_email=not is_draft, # skip notification for draft comments
1595 send_email=not is_draft, # skip notification for draft comments
1608 )
1596 )
1609 is_inline = comment.is_inline
1597 is_inline = comment.is_inline
1610
1598
1611 if allowed_to_change_status:
1599 if allowed_to_change_status:
1612 # calculate old status before we change it
1600 # calculate old status before we change it
1613 old_calculated_status = pull_request.calculated_review_status()
1601 old_calculated_status = pull_request.calculated_review_status()
1614
1602
1615 # get status if set !
1603 # get status if set !
1616 if status:
1604 if status:
1617 ChangesetStatusModel().set_status(
1605 ChangesetStatusModel().set_status(
1618 self.db_repo.repo_id,
1606 self.db_repo.repo_id,
1619 status,
1607 status,
1620 self._rhodecode_user.user_id,
1608 self._rhodecode_user.user_id,
1621 comment,
1609 comment,
1622 pull_request=pull_request
1610 pull_request=pull_request
1623 )
1611 )
1624
1612
1625 Session().flush()
1613 Session().flush()
1626 # this is somehow required to get access to some relationship
1614 # this is somehow required to get access to some relationship
1627 # loaded on comment
1615 # loaded on comment
1628 Session().refresh(comment)
1616 Session().refresh(comment)
1629
1617
1630 PullRequestModel().trigger_pull_request_hook(
1618 PullRequestModel().trigger_pull_request_hook(
1631 pull_request, self._rhodecode_user, 'comment',
1619 pull_request, self._rhodecode_user, 'comment',
1632 data={'comment': comment})
1620 data={'comment': comment})
1633
1621
1634 # we now calculate the status of pull request, and based on that
1622 # we now calculate the status of pull request, and based on that
1635 # calculation we set the commits status
1623 # calculation we set the commits status
1636 calculated_status = pull_request.calculated_review_status()
1624 calculated_status = pull_request.calculated_review_status()
1637 if old_calculated_status != calculated_status:
1625 if old_calculated_status != calculated_status:
1638 PullRequestModel().trigger_pull_request_hook(
1626 PullRequestModel().trigger_pull_request_hook(
1639 pull_request, self._rhodecode_user, 'review_status_change',
1627 pull_request, self._rhodecode_user, 'review_status_change',
1640 data={'status': calculated_status})
1628 data={'status': calculated_status})
1641
1629
1642 Session().commit()
1630 comment_id = comment.comment_id
1631 data[comment_id] = {
1632 'target_id': target_elem_id
1633 }
1634 Session().flush()
1643
1635
1644 data = {
1645 'target_id': h.safeid(h.safe_unicode(
1646 self.request.POST.get('f_path'))),
1647 }
1648
1649 if comment:
1650 c.co = comment
1636 c.co = comment
1651 c.at_version_num = None
1637 c.at_version_num = None
1638 c.is_new = True
1652 rendered_comment = render(
1639 rendered_comment = render(
1653 'rhodecode:templates/changeset/changeset_comment_block.mako',
1640 'rhodecode:templates/changeset/changeset_comment_block.mako',
1654 self._get_template_context(c), self.request)
1641 self._get_template_context(c), self.request)
1655
1642
1656 data.update(comment.get_dict())
1643 data[comment_id].update(comment.get_dict())
1657 data.update({'rendered_text': rendered_comment})
1644 data[comment_id].update({'rendered_text': rendered_comment})
1645
1646 Session().commit()
1658
1647
1659 # skip channelstream for draft comments
1648 # skip channelstream for draft comments
1660 if not is_draft:
1649 if all_drafts:
1661 comment_broadcast_channel = channelstream.comment_channel(
1650 comment_broadcast_channel = channelstream.comment_channel(
1662 self.db_repo_name, pull_request_obj=pull_request)
1651 self.db_repo_name, pull_request_obj=pull_request)
1663
1652
1664 comment_data = data
1653 comment_data = data
1665 comment_type = 'inline' if is_inline else 'general'
1654 comment_type = 'inline' if is_inline else 'general'
1655 if len(data) == 1:
1656 msg = _('posted {} new {} comment').format(len(data), comment_type)
1657 else:
1658 msg = _('posted {} new {} comments').format(len(data), comment_type)
1659
1666 channelstream.comment_channelstream_push(
1660 channelstream.comment_channelstream_push(
1667 self.request, comment_broadcast_channel, self._rhodecode_user,
1661 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1668 _('posted a new {} comment').format(comment_type),
1669 comment_data=comment_data)
1662 comment_data=comment_data)
1670
1663
1671 return data
1664 return data
1672
1665
1673 @LoginRequired()
1666 @LoginRequired()
1674 @NotAnonymous()
1667 @NotAnonymous()
1675 @HasRepoPermissionAnyDecorator(
1668 @HasRepoPermissionAnyDecorator(
1676 'repository.read', 'repository.write', 'repository.admin')
1669 'repository.read', 'repository.write', 'repository.admin')
1677 @CSRFRequired()
1670 @CSRFRequired()
1678 @view_config(
1671 @view_config(
1672 route_name='pullrequest_comment_create', request_method='POST',
1673 renderer='json_ext')
1674 def pull_request_comment_create(self):
1675 _ = self.request.translate
1676
1677 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1678
1679 if pull_request.is_closed():
1680 log.debug('comment: forbidden because pull request is closed')
1681 raise HTTPForbidden()
1682
1683 allowed_to_comment = PullRequestModel().check_user_comment(
1684 pull_request, self._rhodecode_user)
1685 if not allowed_to_comment:
1686 log.debug('comment: forbidden because pull request is from forbidden repo')
1687 raise HTTPForbidden()
1688
1689 comment_data = {
1690 'comment_type': self.request.POST.get('comment_type'),
1691 'text': self.request.POST.get('text'),
1692 'status': self.request.POST.get('changeset_status', None),
1693 'is_draft': self.request.POST.get('draft'),
1694 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 'f_path': self.request.POST.get('f_path'),
1697 'line': self.request.POST.get('line'),
1698 }
1699 data = self._pull_request_comments_create(pull_request, [comment_data])
1700
1701 return data
1702
1703 @LoginRequired()
1704 @NotAnonymous()
1705 @HasRepoPermissionAnyDecorator(
1706 'repository.read', 'repository.write', 'repository.admin')
1707 @CSRFRequired()
1708 @view_config(
1679 route_name='pullrequest_comment_delete', request_method='POST',
1709 route_name='pullrequest_comment_delete', request_method='POST',
1680 renderer='json_ext')
1710 renderer='json_ext')
1681 def pull_request_comment_delete(self):
1711 def pull_request_comment_delete(self):
1682 pull_request = PullRequest.get_or_404(
1712 pull_request = PullRequest.get_or_404(
1683 self.request.matchdict['pull_request_id'])
1713 self.request.matchdict['pull_request_id'])
1684
1714
1685 comment = ChangesetComment.get_or_404(
1715 comment = ChangesetComment.get_or_404(
1686 self.request.matchdict['comment_id'])
1716 self.request.matchdict['comment_id'])
1687 comment_id = comment.comment_id
1717 comment_id = comment.comment_id
1688
1718
1689 if comment.immutable:
1719 if comment.immutable:
1690 # don't allow deleting comments that are immutable
1720 # don't allow deleting comments that are immutable
1691 raise HTTPForbidden()
1721 raise HTTPForbidden()
1692
1722
1693 if pull_request.is_closed():
1723 if pull_request.is_closed():
1694 log.debug('comment: forbidden because pull request is closed')
1724 log.debug('comment: forbidden because pull request is closed')
1695 raise HTTPForbidden()
1725 raise HTTPForbidden()
1696
1726
1697 if not comment:
1727 if not comment:
1698 log.debug('Comment with id:%s not found, skipping', comment_id)
1728 log.debug('Comment with id:%s not found, skipping', comment_id)
1699 # comment already deleted in another call probably
1729 # comment already deleted in another call probably
1700 return True
1730 return True
1701
1731
1702 if comment.pull_request.is_closed():
1732 if comment.pull_request.is_closed():
1703 # don't allow deleting comments on closed pull request
1733 # don't allow deleting comments on closed pull request
1704 raise HTTPForbidden()
1734 raise HTTPForbidden()
1705
1735
1706 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1736 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1707 super_admin = h.HasPermissionAny('hg.admin')()
1737 super_admin = h.HasPermissionAny('hg.admin')()
1708 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1738 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1709 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1739 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1710 comment_repo_admin = is_repo_admin and is_repo_comment
1740 comment_repo_admin = is_repo_admin and is_repo_comment
1711
1741
1712 if super_admin or comment_owner or comment_repo_admin:
1742 if super_admin or comment_owner or comment_repo_admin:
1713 old_calculated_status = comment.pull_request.calculated_review_status()
1743 old_calculated_status = comment.pull_request.calculated_review_status()
1714 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1744 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1715 Session().commit()
1745 Session().commit()
1716 calculated_status = comment.pull_request.calculated_review_status()
1746 calculated_status = comment.pull_request.calculated_review_status()
1717 if old_calculated_status != calculated_status:
1747 if old_calculated_status != calculated_status:
1718 PullRequestModel().trigger_pull_request_hook(
1748 PullRequestModel().trigger_pull_request_hook(
1719 comment.pull_request, self._rhodecode_user, 'review_status_change',
1749 comment.pull_request, self._rhodecode_user, 'review_status_change',
1720 data={'status': calculated_status})
1750 data={'status': calculated_status})
1721 return True
1751 return True
1722 else:
1752 else:
1723 log.warning('No permissions for user %s to delete comment_id: %s',
1753 log.warning('No permissions for user %s to delete comment_id: %s',
1724 self._rhodecode_db_user, comment_id)
1754 self._rhodecode_db_user, comment_id)
1725 raise HTTPNotFound()
1755 raise HTTPNotFound()
1726
1756
1727 @LoginRequired()
1757 @LoginRequired()
1728 @NotAnonymous()
1758 @NotAnonymous()
1729 @HasRepoPermissionAnyDecorator(
1759 @HasRepoPermissionAnyDecorator(
1730 'repository.read', 'repository.write', 'repository.admin')
1760 'repository.read', 'repository.write', 'repository.admin')
1731 @CSRFRequired()
1761 @CSRFRequired()
1732 @view_config(
1762 @view_config(
1733 route_name='pullrequest_comment_edit', request_method='POST',
1763 route_name='pullrequest_comment_edit', request_method='POST',
1734 renderer='json_ext')
1764 renderer='json_ext')
1735 def pull_request_comment_edit(self):
1765 def pull_request_comment_edit(self):
1736 self.load_default_context()
1766 self.load_default_context()
1737
1767
1738 pull_request = PullRequest.get_or_404(
1768 pull_request = PullRequest.get_or_404(
1739 self.request.matchdict['pull_request_id']
1769 self.request.matchdict['pull_request_id']
1740 )
1770 )
1741 comment = ChangesetComment.get_or_404(
1771 comment = ChangesetComment.get_or_404(
1742 self.request.matchdict['comment_id']
1772 self.request.matchdict['comment_id']
1743 )
1773 )
1744 comment_id = comment.comment_id
1774 comment_id = comment.comment_id
1745
1775
1746 if comment.immutable:
1776 if comment.immutable:
1747 # don't allow deleting comments that are immutable
1777 # don't allow deleting comments that are immutable
1748 raise HTTPForbidden()
1778 raise HTTPForbidden()
1749
1779
1750 if pull_request.is_closed():
1780 if pull_request.is_closed():
1751 log.debug('comment: forbidden because pull request is closed')
1781 log.debug('comment: forbidden because pull request is closed')
1752 raise HTTPForbidden()
1782 raise HTTPForbidden()
1753
1783
1754 if not comment:
1784 if not comment:
1755 log.debug('Comment with id:%s not found, skipping', comment_id)
1785 log.debug('Comment with id:%s not found, skipping', comment_id)
1756 # comment already deleted in another call probably
1786 # comment already deleted in another call probably
1757 return True
1787 return True
1758
1788
1759 if comment.pull_request.is_closed():
1789 if comment.pull_request.is_closed():
1760 # don't allow deleting comments on closed pull request
1790 # don't allow deleting comments on closed pull request
1761 raise HTTPForbidden()
1791 raise HTTPForbidden()
1762
1792
1763 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1793 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1764 super_admin = h.HasPermissionAny('hg.admin')()
1794 super_admin = h.HasPermissionAny('hg.admin')()
1765 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1795 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1766 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1796 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1767 comment_repo_admin = is_repo_admin and is_repo_comment
1797 comment_repo_admin = is_repo_admin and is_repo_comment
1768
1798
1769 if super_admin or comment_owner or comment_repo_admin:
1799 if super_admin or comment_owner or comment_repo_admin:
1770 text = self.request.POST.get('text')
1800 text = self.request.POST.get('text')
1771 version = self.request.POST.get('version')
1801 version = self.request.POST.get('version')
1772 if text == comment.text:
1802 if text == comment.text:
1773 log.warning(
1803 log.warning(
1774 'Comment(PR): '
1804 'Comment(PR): '
1775 'Trying to create new version '
1805 'Trying to create new version '
1776 'with the same comment body {}'.format(
1806 'with the same comment body {}'.format(
1777 comment_id,
1807 comment_id,
1778 )
1808 )
1779 )
1809 )
1780 raise HTTPNotFound()
1810 raise HTTPNotFound()
1781
1811
1782 if version.isdigit():
1812 if version.isdigit():
1783 version = int(version)
1813 version = int(version)
1784 else:
1814 else:
1785 log.warning(
1815 log.warning(
1786 'Comment(PR): Wrong version type {} {} '
1816 'Comment(PR): Wrong version type {} {} '
1787 'for comment {}'.format(
1817 'for comment {}'.format(
1788 version,
1818 version,
1789 type(version),
1819 type(version),
1790 comment_id,
1820 comment_id,
1791 )
1821 )
1792 )
1822 )
1793 raise HTTPNotFound()
1823 raise HTTPNotFound()
1794
1824
1795 try:
1825 try:
1796 comment_history = CommentsModel().edit(
1826 comment_history = CommentsModel().edit(
1797 comment_id=comment_id,
1827 comment_id=comment_id,
1798 text=text,
1828 text=text,
1799 auth_user=self._rhodecode_user,
1829 auth_user=self._rhodecode_user,
1800 version=version,
1830 version=version,
1801 )
1831 )
1802 except CommentVersionMismatch:
1832 except CommentVersionMismatch:
1803 raise HTTPConflict()
1833 raise HTTPConflict()
1804
1834
1805 if not comment_history:
1835 if not comment_history:
1806 raise HTTPNotFound()
1836 raise HTTPNotFound()
1807
1837
1808 Session().commit()
1838 Session().commit()
1809
1839
1810 PullRequestModel().trigger_pull_request_hook(
1840 PullRequestModel().trigger_pull_request_hook(
1811 pull_request, self._rhodecode_user, 'comment_edit',
1841 pull_request, self._rhodecode_user, 'comment_edit',
1812 data={'comment': comment})
1842 data={'comment': comment})
1813
1843
1814 return {
1844 return {
1815 'comment_history_id': comment_history.comment_history_id,
1845 'comment_history_id': comment_history.comment_history_id,
1816 'comment_id': comment.comment_id,
1846 'comment_id': comment.comment_id,
1817 'comment_version': comment_history.version,
1847 'comment_version': comment_history.version,
1818 'comment_author_username': comment_history.author.username,
1848 'comment_author_username': comment_history.author.username,
1819 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1849 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1820 'comment_created_on': h.age_component(comment_history.created_on,
1850 'comment_created_on': h.age_component(comment_history.created_on,
1821 time_is_local=True),
1851 time_is_local=True),
1822 }
1852 }
1823 else:
1853 else:
1824 log.warning('No permissions for user %s to edit comment_id: %s',
1854 log.warning('No permissions for user %s to edit comment_id: %s',
1825 self._rhodecode_db_user, comment_id)
1855 self._rhodecode_db_user, comment_id)
1826 raise HTTPNotFound()
1856 raise HTTPNotFound()
@@ -1,371 +1,370 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import hashlib
22 import hashlib
23 import itsdangerous
23 import itsdangerous
24 import logging
24 import logging
25 import requests
25 import requests
26 import datetime
26 import datetime
27
27
28 from dogpile.core import ReadWriteMutex
28 from dogpile.core import ReadWriteMutex
29 from pyramid.threadlocal import get_current_registry
29 from pyramid.threadlocal import get_current_registry
30
30
31 import rhodecode.lib.helpers as h
31 import rhodecode.lib.helpers as h
32 from rhodecode.lib.auth import HasRepoPermissionAny
32 from rhodecode.lib.auth import HasRepoPermissionAny
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.model.db import User
34 from rhodecode.model.db import User
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38 LOCK = ReadWriteMutex()
38 LOCK = ReadWriteMutex()
39
39
40 USER_STATE_PUBLIC_KEYS = [
40 USER_STATE_PUBLIC_KEYS = [
41 'id', 'username', 'first_name', 'last_name',
41 'id', 'username', 'first_name', 'last_name',
42 'icon_link', 'display_name', 'display_link']
42 'icon_link', 'display_name', 'display_link']
43
43
44
44
45 class ChannelstreamException(Exception):
45 class ChannelstreamException(Exception):
46 pass
46 pass
47
47
48
48
49 class ChannelstreamConnectionException(ChannelstreamException):
49 class ChannelstreamConnectionException(ChannelstreamException):
50 pass
50 pass
51
51
52
52
53 class ChannelstreamPermissionException(ChannelstreamException):
53 class ChannelstreamPermissionException(ChannelstreamException):
54 pass
54 pass
55
55
56
56
57 def get_channelstream_server_url(config, endpoint):
57 def get_channelstream_server_url(config, endpoint):
58 return 'http://{}{}'.format(config['server'], endpoint)
58 return 'http://{}{}'.format(config['server'], endpoint)
59
59
60
60
61 def channelstream_request(config, payload, endpoint, raise_exc=True):
61 def channelstream_request(config, payload, endpoint, raise_exc=True):
62 signer = itsdangerous.TimestampSigner(config['secret'])
62 signer = itsdangerous.TimestampSigner(config['secret'])
63 sig_for_server = signer.sign(endpoint)
63 sig_for_server = signer.sign(endpoint)
64 secret_headers = {'x-channelstream-secret': sig_for_server,
64 secret_headers = {'x-channelstream-secret': sig_for_server,
65 'x-channelstream-endpoint': endpoint,
65 'x-channelstream-endpoint': endpoint,
66 'Content-Type': 'application/json'}
66 'Content-Type': 'application/json'}
67 req_url = get_channelstream_server_url(config, endpoint)
67 req_url = get_channelstream_server_url(config, endpoint)
68
68
69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
69 log.debug('Sending a channelstream request to endpoint: `%s`', req_url)
70 response = None
70 response = None
71 try:
71 try:
72 response = requests.post(req_url, data=json.dumps(payload),
72 response = requests.post(req_url, data=json.dumps(payload),
73 headers=secret_headers).json()
73 headers=secret_headers).json()
74 except requests.ConnectionError:
74 except requests.ConnectionError:
75 log.exception('ConnectionError occurred for endpoint %s', req_url)
75 log.exception('ConnectionError occurred for endpoint %s', req_url)
76 if raise_exc:
76 if raise_exc:
77 raise ChannelstreamConnectionException(req_url)
77 raise ChannelstreamConnectionException(req_url)
78 except Exception:
78 except Exception:
79 log.exception('Exception related to Channelstream happened')
79 log.exception('Exception related to Channelstream happened')
80 if raise_exc:
80 if raise_exc:
81 raise ChannelstreamConnectionException()
81 raise ChannelstreamConnectionException()
82 log.debug('Got channelstream response: %s', response)
82 log.debug('Got channelstream response: %s', response)
83 return response
83 return response
84
84
85
85
86 def get_user_data(user_id):
86 def get_user_data(user_id):
87 user = User.get(user_id)
87 user = User.get(user_id)
88 return {
88 return {
89 'id': user.user_id,
89 'id': user.user_id,
90 'username': user.username,
90 'username': user.username,
91 'first_name': user.first_name,
91 'first_name': user.first_name,
92 'last_name': user.last_name,
92 'last_name': user.last_name,
93 'icon_link': h.gravatar_url(user.email, 60),
93 'icon_link': h.gravatar_url(user.email, 60),
94 'display_name': h.person(user, 'username_or_name_or_email'),
94 'display_name': h.person(user, 'username_or_name_or_email'),
95 'display_link': h.link_to_user(user),
95 'display_link': h.link_to_user(user),
96 'notifications': user.user_data.get('notification_status', True)
96 'notifications': user.user_data.get('notification_status', True)
97 }
97 }
98
98
99
99
100 def broadcast_validator(channel_name):
100 def broadcast_validator(channel_name):
101 """ checks if user can access the broadcast channel """
101 """ checks if user can access the broadcast channel """
102 if channel_name == 'broadcast':
102 if channel_name == 'broadcast':
103 return True
103 return True
104
104
105
105
106 def repo_validator(channel_name):
106 def repo_validator(channel_name):
107 """ checks if user can access the broadcast channel """
107 """ checks if user can access the broadcast channel """
108 channel_prefix = '/repo$'
108 channel_prefix = '/repo$'
109 if channel_name.startswith(channel_prefix):
109 if channel_name.startswith(channel_prefix):
110 elements = channel_name[len(channel_prefix):].split('$')
110 elements = channel_name[len(channel_prefix):].split('$')
111 repo_name = elements[0]
111 repo_name = elements[0]
112 can_access = HasRepoPermissionAny(
112 can_access = HasRepoPermissionAny(
113 'repository.read',
113 'repository.read',
114 'repository.write',
114 'repository.write',
115 'repository.admin')(repo_name)
115 'repository.admin')(repo_name)
116 log.debug(
116 log.debug(
117 'permission check for %s channel resulted in %s',
117 'permission check for %s channel resulted in %s',
118 repo_name, can_access)
118 repo_name, can_access)
119 if can_access:
119 if can_access:
120 return True
120 return True
121 return False
121 return False
122
122
123
123
124 def check_channel_permissions(channels, plugin_validators, should_raise=True):
124 def check_channel_permissions(channels, plugin_validators, should_raise=True):
125 valid_channels = []
125 valid_channels = []
126
126
127 validators = [broadcast_validator, repo_validator]
127 validators = [broadcast_validator, repo_validator]
128 if plugin_validators:
128 if plugin_validators:
129 validators.extend(plugin_validators)
129 validators.extend(plugin_validators)
130 for channel_name in channels:
130 for channel_name in channels:
131 is_valid = False
131 is_valid = False
132 for validator in validators:
132 for validator in validators:
133 if validator(channel_name):
133 if validator(channel_name):
134 is_valid = True
134 is_valid = True
135 break
135 break
136 if is_valid:
136 if is_valid:
137 valid_channels.append(channel_name)
137 valid_channels.append(channel_name)
138 else:
138 else:
139 if should_raise:
139 if should_raise:
140 raise ChannelstreamPermissionException()
140 raise ChannelstreamPermissionException()
141 return valid_channels
141 return valid_channels
142
142
143
143
144 def get_channels_info(self, channels):
144 def get_channels_info(self, channels):
145 payload = {'channels': channels}
145 payload = {'channels': channels}
146 # gather persistence info
146 # gather persistence info
147 return channelstream_request(self._config(), payload, '/info')
147 return channelstream_request(self._config(), payload, '/info')
148
148
149
149
150 def parse_channels_info(info_result, include_channel_info=None):
150 def parse_channels_info(info_result, include_channel_info=None):
151 """
151 """
152 Returns data that contains only secure information that can be
152 Returns data that contains only secure information that can be
153 presented to clients
153 presented to clients
154 """
154 """
155 include_channel_info = include_channel_info or []
155 include_channel_info = include_channel_info or []
156
156
157 user_state_dict = {}
157 user_state_dict = {}
158 for userinfo in info_result['users']:
158 for userinfo in info_result['users']:
159 user_state_dict[userinfo['user']] = {
159 user_state_dict[userinfo['user']] = {
160 k: v for k, v in userinfo['state'].items()
160 k: v for k, v in userinfo['state'].items()
161 if k in USER_STATE_PUBLIC_KEYS
161 if k in USER_STATE_PUBLIC_KEYS
162 }
162 }
163
163
164 channels_info = {}
164 channels_info = {}
165
165
166 for c_name, c_info in info_result['channels'].items():
166 for c_name, c_info in info_result['channels'].items():
167 if c_name not in include_channel_info:
167 if c_name not in include_channel_info:
168 continue
168 continue
169 connected_list = []
169 connected_list = []
170 for username in c_info['users']:
170 for username in c_info['users']:
171 connected_list.append({
171 connected_list.append({
172 'user': username,
172 'user': username,
173 'state': user_state_dict[username]
173 'state': user_state_dict[username]
174 })
174 })
175 channels_info[c_name] = {'users': connected_list,
175 channels_info[c_name] = {'users': connected_list,
176 'history': c_info['history']}
176 'history': c_info['history']}
177
177
178 return channels_info
178 return channels_info
179
179
180
180
181 def log_filepath(history_location, channel_name):
181 def log_filepath(history_location, channel_name):
182 hasher = hashlib.sha256()
182 hasher = hashlib.sha256()
183 hasher.update(channel_name.encode('utf8'))
183 hasher.update(channel_name.encode('utf8'))
184 filename = '{}.log'.format(hasher.hexdigest())
184 filename = '{}.log'.format(hasher.hexdigest())
185 filepath = os.path.join(history_location, filename)
185 filepath = os.path.join(history_location, filename)
186 return filepath
186 return filepath
187
187
188
188
189 def read_history(history_location, channel_name):
189 def read_history(history_location, channel_name):
190 filepath = log_filepath(history_location, channel_name)
190 filepath = log_filepath(history_location, channel_name)
191 if not os.path.exists(filepath):
191 if not os.path.exists(filepath):
192 return []
192 return []
193 history_lines_limit = -100
193 history_lines_limit = -100
194 history = []
194 history = []
195 with open(filepath, 'rb') as f:
195 with open(filepath, 'rb') as f:
196 for line in f.readlines()[history_lines_limit:]:
196 for line in f.readlines()[history_lines_limit:]:
197 try:
197 try:
198 history.append(json.loads(line))
198 history.append(json.loads(line))
199 except Exception:
199 except Exception:
200 log.exception('Failed to load history')
200 log.exception('Failed to load history')
201 return history
201 return history
202
202
203
203
204 def update_history_from_logs(config, channels, payload):
204 def update_history_from_logs(config, channels, payload):
205 history_location = config.get('history.location')
205 history_location = config.get('history.location')
206 for channel in channels:
206 for channel in channels:
207 history = read_history(history_location, channel)
207 history = read_history(history_location, channel)
208 payload['channels_info'][channel]['history'] = history
208 payload['channels_info'][channel]['history'] = history
209
209
210
210
211 def write_history(config, message):
211 def write_history(config, message):
212 """ writes a messge to a base64encoded filename """
212 """ writes a messge to a base64encoded filename """
213 history_location = config.get('history.location')
213 history_location = config.get('history.location')
214 if not os.path.exists(history_location):
214 if not os.path.exists(history_location):
215 return
215 return
216 try:
216 try:
217 LOCK.acquire_write_lock()
217 LOCK.acquire_write_lock()
218 filepath = log_filepath(history_location, message['channel'])
218 filepath = log_filepath(history_location, message['channel'])
219 with open(filepath, 'ab') as f:
219 with open(filepath, 'ab') as f:
220 json.dump(message, f)
220 json.dump(message, f)
221 f.write('\n')
221 f.write('\n')
222 finally:
222 finally:
223 LOCK.release_write_lock()
223 LOCK.release_write_lock()
224
224
225
225
226 def get_connection_validators(registry):
226 def get_connection_validators(registry):
227 validators = []
227 validators = []
228 for k, config in registry.rhodecode_plugins.items():
228 for k, config in registry.rhodecode_plugins.items():
229 validator = config.get('channelstream', {}).get('connect_validator')
229 validator = config.get('channelstream', {}).get('connect_validator')
230 if validator:
230 if validator:
231 validators.append(validator)
231 validators.append(validator)
232 return validators
232 return validators
233
233
234
234
235 def get_channelstream_config(registry=None):
235 def get_channelstream_config(registry=None):
236 if not registry:
236 if not registry:
237 registry = get_current_registry()
237 registry = get_current_registry()
238
238
239 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
239 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
240 channelstream_config = rhodecode_plugins.get('channelstream', {})
240 channelstream_config = rhodecode_plugins.get('channelstream', {})
241 return channelstream_config
241 return channelstream_config
242
242
243
243
244 def post_message(channel, message, username, registry=None):
244 def post_message(channel, message, username, registry=None):
245 channelstream_config = get_channelstream_config(registry)
245 channelstream_config = get_channelstream_config(registry)
246 if not channelstream_config.get('enabled'):
246 if not channelstream_config.get('enabled'):
247 return
247 return
248
248
249 message_obj = message
249 message_obj = message
250 if isinstance(message, basestring):
250 if isinstance(message, basestring):
251 message_obj = {
251 message_obj = {
252 'message': message,
252 'message': message,
253 'level': 'success',
253 'level': 'success',
254 'topic': '/notifications'
254 'topic': '/notifications'
255 }
255 }
256
256
257 log.debug('Channelstream: sending notification to channel %s', channel)
257 log.debug('Channelstream: sending notification to channel %s', channel)
258 payload = {
258 payload = {
259 'type': 'message',
259 'type': 'message',
260 'timestamp': datetime.datetime.utcnow(),
260 'timestamp': datetime.datetime.utcnow(),
261 'user': 'system',
261 'user': 'system',
262 'exclude_users': [username],
262 'exclude_users': [username],
263 'channel': channel,
263 'channel': channel,
264 'message': message_obj
264 'message': message_obj
265 }
265 }
266
266
267 try:
267 try:
268 return channelstream_request(
268 return channelstream_request(
269 channelstream_config, [payload], '/message',
269 channelstream_config, [payload], '/message',
270 raise_exc=False)
270 raise_exc=False)
271 except ChannelstreamException:
271 except ChannelstreamException:
272 log.exception('Failed to send channelstream data')
272 log.exception('Failed to send channelstream data')
273 raise
273 raise
274
274
275
275
276 def _reload_link(label):
276 def _reload_link(label):
277 return (
277 return (
278 '<a onclick="window.location.reload()">'
278 '<a onclick="window.location.reload()">'
279 '<strong>{}</strong>'
279 '<strong>{}</strong>'
280 '</a>'.format(label)
280 '</a>'.format(label)
281 )
281 )
282
282
283
283
284 def pr_channel(pull_request):
284 def pr_channel(pull_request):
285 repo_name = pull_request.target_repo.repo_name
285 repo_name = pull_request.target_repo.repo_name
286 pull_request_id = pull_request.pull_request_id
286 pull_request_id = pull_request.pull_request_id
287 channel = '/repo${}$/pr/{}'.format(repo_name, pull_request_id)
287 channel = '/repo${}$/pr/{}'.format(repo_name, pull_request_id)
288 log.debug('Getting pull-request channelstream broadcast channel: %s', channel)
288 log.debug('Getting pull-request channelstream broadcast channel: %s', channel)
289 return channel
289 return channel
290
290
291
291
292 def comment_channel(repo_name, commit_obj=None, pull_request_obj=None):
292 def comment_channel(repo_name, commit_obj=None, pull_request_obj=None):
293 channel = None
293 channel = None
294 if commit_obj:
294 if commit_obj:
295 channel = u'/repo${}$/commit/{}'.format(
295 channel = u'/repo${}$/commit/{}'.format(
296 repo_name, commit_obj.raw_id
296 repo_name, commit_obj.raw_id
297 )
297 )
298 elif pull_request_obj:
298 elif pull_request_obj:
299 channel = u'/repo${}$/pr/{}'.format(
299 channel = u'/repo${}$/pr/{}'.format(
300 repo_name, pull_request_obj.pull_request_id
300 repo_name, pull_request_obj.pull_request_id
301 )
301 )
302 log.debug('Getting comment channelstream broadcast channel: %s', channel)
302 log.debug('Getting comment channelstream broadcast channel: %s', channel)
303
303
304 return channel
304 return channel
305
305
306
306
307 def pr_update_channelstream_push(request, pr_broadcast_channel, user, msg, **kwargs):
307 def pr_update_channelstream_push(request, pr_broadcast_channel, user, msg, **kwargs):
308 """
308 """
309 Channel push on pull request update
309 Channel push on pull request update
310 """
310 """
311 if not pr_broadcast_channel:
311 if not pr_broadcast_channel:
312 return
312 return
313
313
314 _ = request.translate
314 _ = request.translate
315
315
316 message = '{} {}'.format(
316 message = '{} {}'.format(
317 msg,
317 msg,
318 _reload_link(_(' Reload page to load changes')))
318 _reload_link(_(' Reload page to load changes')))
319
319
320 message_obj = {
320 message_obj = {
321 'message': message,
321 'message': message,
322 'level': 'success',
322 'level': 'success',
323 'topic': '/notifications'
323 'topic': '/notifications'
324 }
324 }
325
325
326 post_message(
326 post_message(
327 pr_broadcast_channel, message_obj, user.username,
327 pr_broadcast_channel, message_obj, user.username,
328 registry=request.registry)
328 registry=request.registry)
329
329
330
330
331 def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **kwargs):
331 def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **kwargs):
332 """
332 """
333 Channelstream push on comment action, on commit, or pull-request
333 Channelstream push on comment action, on commit, or pull-request
334 """
334 """
335 if not comment_broadcast_channel:
335 if not comment_broadcast_channel:
336 return
336 return
337
337
338 _ = request.translate
338 _ = request.translate
339
339
340 comment_data = kwargs.pop('comment_data', {})
340 comment_data = kwargs.pop('comment_data', {})
341 user_data = kwargs.pop('user_data', {})
341 user_data = kwargs.pop('user_data', {})
342 comment_id = comment_data.get('comment_id')
342 comment_id = comment_data.keys()[0] if comment_data else ''
343
343
344 message = '<strong>{}</strong> {} #{}, {}'.format(
344 message = '<strong>{}</strong> {} #{}'.format(
345 user.username,
345 user.username,
346 msg,
346 msg,
347 comment_id,
347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 )
348 )
350
349
351 message_obj = {
350 message_obj = {
352 'message': message,
351 'message': message,
353 'level': 'success',
352 'level': 'success',
354 'topic': '/notifications'
353 'topic': '/notifications'
355 }
354 }
356
355
357 post_message(
356 post_message(
358 comment_broadcast_channel, message_obj, user.username,
357 comment_broadcast_channel, message_obj, user.username,
359 registry=request.registry)
358 registry=request.registry)
360
359
361 message_obj = {
360 message_obj = {
362 'message': None,
361 'message': None,
363 'user': user.username,
362 'user': user.username,
364 'comment_id': comment_id,
363 'comment_id': comment_id,
365 'comment_data': comment_data,
364 'comment_data': comment_data,
366 'user_data': user_data,
365 'user_data': user_data,
367 'topic': '/comment'
366 'topic': '/comment'
368 }
367 }
369 post_message(
368 post_message(
370 comment_broadcast_channel, message_obj, user.username,
369 comment_broadcast_channel, message_obj, user.username,
371 registry=request.registry)
370 registry=request.registry)
@@ -1,1272 +1,1272 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Set of diffing helpers, previously part of vcs
23 Set of diffing helpers, previously part of vcs
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import bz2
28 import bz2
29 import gzip
29 import gzip
30 import time
30 import time
31
31
32 import collections
32 import collections
33 import difflib
33 import difflib
34 import logging
34 import logging
35 import cPickle as pickle
35 import cPickle as pickle
36 from itertools import tee, imap
36 from itertools import tee, imap
37
37
38 from rhodecode.lib.vcs.exceptions import VCSError
38 from rhodecode.lib.vcs.exceptions import VCSError
39 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
39 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 # define max context, a file with more than this numbers of lines is unusable
44 # define max context, a file with more than this numbers of lines is unusable
45 # in browser anyway
45 # in browser anyway
46 MAX_CONTEXT = 20 * 1024
46 MAX_CONTEXT = 20 * 1024
47 DEFAULT_CONTEXT = 3
47 DEFAULT_CONTEXT = 3
48
48
49
49
50 def get_diff_context(request):
50 def get_diff_context(request):
51 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
51 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
52
52
53
53
54 def get_diff_whitespace_flag(request):
54 def get_diff_whitespace_flag(request):
55 return request.GET.get('ignorews', '') == '1'
55 return request.GET.get('ignorews', '') == '1'
56
56
57
57
58 class OPS(object):
58 class OPS(object):
59 ADD = 'A'
59 ADD = 'A'
60 MOD = 'M'
60 MOD = 'M'
61 DEL = 'D'
61 DEL = 'D'
62
62
63
63
64 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
64 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
65 """
65 """
66 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
66 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
67
67
68 :param ignore_whitespace: ignore whitespaces in diff
68 :param ignore_whitespace: ignore whitespaces in diff
69 """
69 """
70 # make sure we pass in default context
70 # make sure we pass in default context
71 context = context or 3
71 context = context or 3
72 # protect against IntOverflow when passing HUGE context
72 # protect against IntOverflow when passing HUGE context
73 if context > MAX_CONTEXT:
73 if context > MAX_CONTEXT:
74 context = MAX_CONTEXT
74 context = MAX_CONTEXT
75
75
76 submodules = filter(lambda o: isinstance(o, SubModuleNode),
76 submodules = filter(lambda o: isinstance(o, SubModuleNode),
77 [filenode_new, filenode_old])
77 [filenode_new, filenode_old])
78 if submodules:
78 if submodules:
79 return ''
79 return ''
80
80
81 for filenode in (filenode_old, filenode_new):
81 for filenode in (filenode_old, filenode_new):
82 if not isinstance(filenode, FileNode):
82 if not isinstance(filenode, FileNode):
83 raise VCSError(
83 raise VCSError(
84 "Given object should be FileNode object, not %s"
84 "Given object should be FileNode object, not %s"
85 % filenode.__class__)
85 % filenode.__class__)
86
86
87 repo = filenode_new.commit.repository
87 repo = filenode_new.commit.repository
88 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
88 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
89 new_commit = filenode_new.commit
89 new_commit = filenode_new.commit
90
90
91 vcs_gitdiff = repo.get_diff(
91 vcs_gitdiff = repo.get_diff(
92 old_commit, new_commit, filenode_new.path,
92 old_commit, new_commit, filenode_new.path,
93 ignore_whitespace, context, path1=filenode_old.path)
93 ignore_whitespace, context, path1=filenode_old.path)
94 return vcs_gitdiff
94 return vcs_gitdiff
95
95
96 NEW_FILENODE = 1
96 NEW_FILENODE = 1
97 DEL_FILENODE = 2
97 DEL_FILENODE = 2
98 MOD_FILENODE = 3
98 MOD_FILENODE = 3
99 RENAMED_FILENODE = 4
99 RENAMED_FILENODE = 4
100 COPIED_FILENODE = 5
100 COPIED_FILENODE = 5
101 CHMOD_FILENODE = 6
101 CHMOD_FILENODE = 6
102 BIN_FILENODE = 7
102 BIN_FILENODE = 7
103
103
104
104
105 class LimitedDiffContainer(object):
105 class LimitedDiffContainer(object):
106
106
107 def __init__(self, diff_limit, cur_diff_size, diff):
107 def __init__(self, diff_limit, cur_diff_size, diff):
108 self.diff = diff
108 self.diff = diff
109 self.diff_limit = diff_limit
109 self.diff_limit = diff_limit
110 self.cur_diff_size = cur_diff_size
110 self.cur_diff_size = cur_diff_size
111
111
112 def __getitem__(self, key):
112 def __getitem__(self, key):
113 return self.diff.__getitem__(key)
113 return self.diff.__getitem__(key)
114
114
115 def __iter__(self):
115 def __iter__(self):
116 for l in self.diff:
116 for l in self.diff:
117 yield l
117 yield l
118
118
119
119
120 class Action(object):
120 class Action(object):
121 """
121 """
122 Contains constants for the action value of the lines in a parsed diff.
122 Contains constants for the action value of the lines in a parsed diff.
123 """
123 """
124
124
125 ADD = 'add'
125 ADD = 'add'
126 DELETE = 'del'
126 DELETE = 'del'
127 UNMODIFIED = 'unmod'
127 UNMODIFIED = 'unmod'
128
128
129 CONTEXT = 'context'
129 CONTEXT = 'context'
130 OLD_NO_NL = 'old-no-nl'
130 OLD_NO_NL = 'old-no-nl'
131 NEW_NO_NL = 'new-no-nl'
131 NEW_NO_NL = 'new-no-nl'
132
132
133
133
134 class DiffProcessor(object):
134 class DiffProcessor(object):
135 """
135 """
136 Give it a unified or git diff and it returns a list of the files that were
136 Give it a unified or git diff and it returns a list of the files that were
137 mentioned in the diff together with a dict of meta information that
137 mentioned in the diff together with a dict of meta information that
138 can be used to render it in a HTML template.
138 can be used to render it in a HTML template.
139
139
140 .. note:: Unicode handling
140 .. note:: Unicode handling
141
141
142 The original diffs are a byte sequence and can contain filenames
142 The original diffs are a byte sequence and can contain filenames
143 in mixed encodings. This class generally returns `unicode` objects
143 in mixed encodings. This class generally returns `unicode` objects
144 since the result is intended for presentation to the user.
144 since the result is intended for presentation to the user.
145
145
146 """
146 """
147 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
147 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
148 _newline_marker = re.compile(r'^\\ No newline at end of file')
148 _newline_marker = re.compile(r'^\\ No newline at end of file')
149
149
150 # used for inline highlighter word split
150 # used for inline highlighter word split
151 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
151 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
152
152
153 # collapse ranges of commits over given number
153 # collapse ranges of commits over given number
154 _collapse_commits_over = 5
154 _collapse_commits_over = 5
155
155
156 def __init__(self, diff, format='gitdiff', diff_limit=None,
156 def __init__(self, diff, format='gitdiff', diff_limit=None,
157 file_limit=None, show_full_diff=True):
157 file_limit=None, show_full_diff=True):
158 """
158 """
159 :param diff: A `Diff` object representing a diff from a vcs backend
159 :param diff: A `Diff` object representing a diff from a vcs backend
160 :param format: format of diff passed, `udiff` or `gitdiff`
160 :param format: format of diff passed, `udiff` or `gitdiff`
161 :param diff_limit: define the size of diff that is considered "big"
161 :param diff_limit: define the size of diff that is considered "big"
162 based on that parameter cut off will be triggered, set to None
162 based on that parameter cut off will be triggered, set to None
163 to show full diff
163 to show full diff
164 """
164 """
165 self._diff = diff
165 self._diff = diff
166 self._format = format
166 self._format = format
167 self.adds = 0
167 self.adds = 0
168 self.removes = 0
168 self.removes = 0
169 # calculate diff size
169 # calculate diff size
170 self.diff_limit = diff_limit
170 self.diff_limit = diff_limit
171 self.file_limit = file_limit
171 self.file_limit = file_limit
172 self.show_full_diff = show_full_diff
172 self.show_full_diff = show_full_diff
173 self.cur_diff_size = 0
173 self.cur_diff_size = 0
174 self.parsed = False
174 self.parsed = False
175 self.parsed_diff = []
175 self.parsed_diff = []
176
176
177 log.debug('Initialized DiffProcessor with %s mode', format)
177 log.debug('Initialized DiffProcessor with %s mode', format)
178 if format == 'gitdiff':
178 if format == 'gitdiff':
179 self.differ = self._highlight_line_difflib
179 self.differ = self._highlight_line_difflib
180 self._parser = self._parse_gitdiff
180 self._parser = self._parse_gitdiff
181 else:
181 else:
182 self.differ = self._highlight_line_udiff
182 self.differ = self._highlight_line_udiff
183 self._parser = self._new_parse_gitdiff
183 self._parser = self._new_parse_gitdiff
184
184
185 def _copy_iterator(self):
185 def _copy_iterator(self):
186 """
186 """
187 make a fresh copy of generator, we should not iterate thru
187 make a fresh copy of generator, we should not iterate thru
188 an original as it's needed for repeating operations on
188 an original as it's needed for repeating operations on
189 this instance of DiffProcessor
189 this instance of DiffProcessor
190 """
190 """
191 self.__udiff, iterator_copy = tee(self.__udiff)
191 self.__udiff, iterator_copy = tee(self.__udiff)
192 return iterator_copy
192 return iterator_copy
193
193
194 def _escaper(self, string):
194 def _escaper(self, string):
195 """
195 """
196 Escaper for diff escapes special chars and checks the diff limit
196 Escaper for diff escapes special chars and checks the diff limit
197
197
198 :param string:
198 :param string:
199 """
199 """
200 self.cur_diff_size += len(string)
200 self.cur_diff_size += len(string)
201
201
202 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
202 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
203 raise DiffLimitExceeded('Diff Limit Exceeded')
203 raise DiffLimitExceeded('Diff Limit Exceeded')
204
204
205 return string \
205 return string \
206 .replace('&', '&amp;')\
206 .replace('&', '&amp;')\
207 .replace('<', '&lt;')\
207 .replace('<', '&lt;')\
208 .replace('>', '&gt;')
208 .replace('>', '&gt;')
209
209
210 def _line_counter(self, l):
210 def _line_counter(self, l):
211 """
211 """
212 Checks each line and bumps total adds/removes for this diff
212 Checks each line and bumps total adds/removes for this diff
213
213
214 :param l:
214 :param l:
215 """
215 """
216 if l.startswith('+') and not l.startswith('+++'):
216 if l.startswith('+') and not l.startswith('+++'):
217 self.adds += 1
217 self.adds += 1
218 elif l.startswith('-') and not l.startswith('---'):
218 elif l.startswith('-') and not l.startswith('---'):
219 self.removes += 1
219 self.removes += 1
220 return safe_unicode(l)
220 return safe_unicode(l)
221
221
222 def _highlight_line_difflib(self, line, next_):
222 def _highlight_line_difflib(self, line, next_):
223 """
223 """
224 Highlight inline changes in both lines.
224 Highlight inline changes in both lines.
225 """
225 """
226
226
227 if line['action'] == Action.DELETE:
227 if line['action'] == Action.DELETE:
228 old, new = line, next_
228 old, new = line, next_
229 else:
229 else:
230 old, new = next_, line
230 old, new = next_, line
231
231
232 oldwords = self._token_re.split(old['line'])
232 oldwords = self._token_re.split(old['line'])
233 newwords = self._token_re.split(new['line'])
233 newwords = self._token_re.split(new['line'])
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
234 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
235
235
236 oldfragments, newfragments = [], []
236 oldfragments, newfragments = [], []
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
237 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
238 oldfrag = ''.join(oldwords[i1:i2])
238 oldfrag = ''.join(oldwords[i1:i2])
239 newfrag = ''.join(newwords[j1:j2])
239 newfrag = ''.join(newwords[j1:j2])
240 if tag != 'equal':
240 if tag != 'equal':
241 if oldfrag:
241 if oldfrag:
242 oldfrag = '<del>%s</del>' % oldfrag
242 oldfrag = '<del>%s</del>' % oldfrag
243 if newfrag:
243 if newfrag:
244 newfrag = '<ins>%s</ins>' % newfrag
244 newfrag = '<ins>%s</ins>' % newfrag
245 oldfragments.append(oldfrag)
245 oldfragments.append(oldfrag)
246 newfragments.append(newfrag)
246 newfragments.append(newfrag)
247
247
248 old['line'] = "".join(oldfragments)
248 old['line'] = "".join(oldfragments)
249 new['line'] = "".join(newfragments)
249 new['line'] = "".join(newfragments)
250
250
251 def _highlight_line_udiff(self, line, next_):
251 def _highlight_line_udiff(self, line, next_):
252 """
252 """
253 Highlight inline changes in both lines.
253 Highlight inline changes in both lines.
254 """
254 """
255 start = 0
255 start = 0
256 limit = min(len(line['line']), len(next_['line']))
256 limit = min(len(line['line']), len(next_['line']))
257 while start < limit and line['line'][start] == next_['line'][start]:
257 while start < limit and line['line'][start] == next_['line'][start]:
258 start += 1
258 start += 1
259 end = -1
259 end = -1
260 limit -= start
260 limit -= start
261 while -end <= limit and line['line'][end] == next_['line'][end]:
261 while -end <= limit and line['line'][end] == next_['line'][end]:
262 end -= 1
262 end -= 1
263 end += 1
263 end += 1
264 if start or end:
264 if start or end:
265 def do(l):
265 def do(l):
266 last = end + len(l['line'])
266 last = end + len(l['line'])
267 if l['action'] == Action.ADD:
267 if l['action'] == Action.ADD:
268 tag = 'ins'
268 tag = 'ins'
269 else:
269 else:
270 tag = 'del'
270 tag = 'del'
271 l['line'] = '%s<%s>%s</%s>%s' % (
271 l['line'] = '%s<%s>%s</%s>%s' % (
272 l['line'][:start],
272 l['line'][:start],
273 tag,
273 tag,
274 l['line'][start:last],
274 l['line'][start:last],
275 tag,
275 tag,
276 l['line'][last:]
276 l['line'][last:]
277 )
277 )
278 do(line)
278 do(line)
279 do(next_)
279 do(next_)
280
280
281 def _clean_line(self, line, command):
281 def _clean_line(self, line, command):
282 if command in ['+', '-', ' ']:
282 if command in ['+', '-', ' ']:
283 # only modify the line if it's actually a diff thing
283 # only modify the line if it's actually a diff thing
284 line = line[1:]
284 line = line[1:]
285 return line
285 return line
286
286
287 def _parse_gitdiff(self, inline_diff=True):
287 def _parse_gitdiff(self, inline_diff=True):
288 _files = []
288 _files = []
289 diff_container = lambda arg: arg
289 diff_container = lambda arg: arg
290
290
291 for chunk in self._diff.chunks():
291 for chunk in self._diff.chunks():
292 head = chunk.header
292 head = chunk.header
293
293
294 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
294 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
295 raw_diff = chunk.raw
295 raw_diff = chunk.raw
296 limited_diff = False
296 limited_diff = False
297 exceeds_limit = False
297 exceeds_limit = False
298
298
299 op = None
299 op = None
300 stats = {
300 stats = {
301 'added': 0,
301 'added': 0,
302 'deleted': 0,
302 'deleted': 0,
303 'binary': False,
303 'binary': False,
304 'ops': {},
304 'ops': {},
305 }
305 }
306
306
307 if head['deleted_file_mode']:
307 if head['deleted_file_mode']:
308 op = OPS.DEL
308 op = OPS.DEL
309 stats['binary'] = True
309 stats['binary'] = True
310 stats['ops'][DEL_FILENODE] = 'deleted file'
310 stats['ops'][DEL_FILENODE] = 'deleted file'
311
311
312 elif head['new_file_mode']:
312 elif head['new_file_mode']:
313 op = OPS.ADD
313 op = OPS.ADD
314 stats['binary'] = True
314 stats['binary'] = True
315 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
315 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
316 else: # modify operation, can be copy, rename or chmod
316 else: # modify operation, can be copy, rename or chmod
317
317
318 # CHMOD
318 # CHMOD
319 if head['new_mode'] and head['old_mode']:
319 if head['new_mode'] and head['old_mode']:
320 op = OPS.MOD
320 op = OPS.MOD
321 stats['binary'] = True
321 stats['binary'] = True
322 stats['ops'][CHMOD_FILENODE] = (
322 stats['ops'][CHMOD_FILENODE] = (
323 'modified file chmod %s => %s' % (
323 'modified file chmod %s => %s' % (
324 head['old_mode'], head['new_mode']))
324 head['old_mode'], head['new_mode']))
325 # RENAME
325 # RENAME
326 if head['rename_from'] != head['rename_to']:
326 if head['rename_from'] != head['rename_to']:
327 op = OPS.MOD
327 op = OPS.MOD
328 stats['binary'] = True
328 stats['binary'] = True
329 stats['ops'][RENAMED_FILENODE] = (
329 stats['ops'][RENAMED_FILENODE] = (
330 'file renamed from %s to %s' % (
330 'file renamed from %s to %s' % (
331 head['rename_from'], head['rename_to']))
331 head['rename_from'], head['rename_to']))
332 # COPY
332 # COPY
333 if head.get('copy_from') and head.get('copy_to'):
333 if head.get('copy_from') and head.get('copy_to'):
334 op = OPS.MOD
334 op = OPS.MOD
335 stats['binary'] = True
335 stats['binary'] = True
336 stats['ops'][COPIED_FILENODE] = (
336 stats['ops'][COPIED_FILENODE] = (
337 'file copied from %s to %s' % (
337 'file copied from %s to %s' % (
338 head['copy_from'], head['copy_to']))
338 head['copy_from'], head['copy_to']))
339
339
340 # If our new parsed headers didn't match anything fallback to
340 # If our new parsed headers didn't match anything fallback to
341 # old style detection
341 # old style detection
342 if op is None:
342 if op is None:
343 if not head['a_file'] and head['b_file']:
343 if not head['a_file'] and head['b_file']:
344 op = OPS.ADD
344 op = OPS.ADD
345 stats['binary'] = True
345 stats['binary'] = True
346 stats['ops'][NEW_FILENODE] = 'new file'
346 stats['ops'][NEW_FILENODE] = 'new file'
347
347
348 elif head['a_file'] and not head['b_file']:
348 elif head['a_file'] and not head['b_file']:
349 op = OPS.DEL
349 op = OPS.DEL
350 stats['binary'] = True
350 stats['binary'] = True
351 stats['ops'][DEL_FILENODE] = 'deleted file'
351 stats['ops'][DEL_FILENODE] = 'deleted file'
352
352
353 # it's not ADD not DELETE
353 # it's not ADD not DELETE
354 if op is None:
354 if op is None:
355 op = OPS.MOD
355 op = OPS.MOD
356 stats['binary'] = True
356 stats['binary'] = True
357 stats['ops'][MOD_FILENODE] = 'modified file'
357 stats['ops'][MOD_FILENODE] = 'modified file'
358
358
359 # a real non-binary diff
359 # a real non-binary diff
360 if head['a_file'] or head['b_file']:
360 if head['a_file'] or head['b_file']:
361 try:
361 try:
362 raw_diff, chunks, _stats = self._parse_lines(diff)
362 raw_diff, chunks, _stats = self._parse_lines(diff)
363 stats['binary'] = False
363 stats['binary'] = False
364 stats['added'] = _stats[0]
364 stats['added'] = _stats[0]
365 stats['deleted'] = _stats[1]
365 stats['deleted'] = _stats[1]
366 # explicit mark that it's a modified file
366 # explicit mark that it's a modified file
367 if op == OPS.MOD:
367 if op == OPS.MOD:
368 stats['ops'][MOD_FILENODE] = 'modified file'
368 stats['ops'][MOD_FILENODE] = 'modified file'
369 exceeds_limit = len(raw_diff) > self.file_limit
369 exceeds_limit = len(raw_diff) > self.file_limit
370
370
371 # changed from _escaper function so we validate size of
371 # changed from _escaper function so we validate size of
372 # each file instead of the whole diff
372 # each file instead of the whole diff
373 # diff will hide big files but still show small ones
373 # diff will hide big files but still show small ones
374 # from my tests, big files are fairly safe to be parsed
374 # from my tests, big files are fairly safe to be parsed
375 # but the browser is the bottleneck
375 # but the browser is the bottleneck
376 if not self.show_full_diff and exceeds_limit:
376 if not self.show_full_diff and exceeds_limit:
377 raise DiffLimitExceeded('File Limit Exceeded')
377 raise DiffLimitExceeded('File Limit Exceeded')
378
378
379 except DiffLimitExceeded:
379 except DiffLimitExceeded:
380 diff_container = lambda _diff: \
380 diff_container = lambda _diff: \
381 LimitedDiffContainer(
381 LimitedDiffContainer(
382 self.diff_limit, self.cur_diff_size, _diff)
382 self.diff_limit, self.cur_diff_size, _diff)
383
383
384 exceeds_limit = len(raw_diff) > self.file_limit
384 exceeds_limit = len(raw_diff) > self.file_limit
385 limited_diff = True
385 limited_diff = True
386 chunks = []
386 chunks = []
387
387
388 else: # GIT format binary patch, or possibly empty diff
388 else: # GIT format binary patch, or possibly empty diff
389 if head['bin_patch']:
389 if head['bin_patch']:
390 # we have operation already extracted, but we mark simply
390 # we have operation already extracted, but we mark simply
391 # it's a diff we wont show for binary files
391 # it's a diff we wont show for binary files
392 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
392 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
393 chunks = []
393 chunks = []
394
394
395 if chunks and not self.show_full_diff and op == OPS.DEL:
395 if chunks and not self.show_full_diff and op == OPS.DEL:
396 # if not full diff mode show deleted file contents
396 # if not full diff mode show deleted file contents
397 # TODO: anderson: if the view is not too big, there is no way
397 # TODO: anderson: if the view is not too big, there is no way
398 # to see the content of the file
398 # to see the content of the file
399 chunks = []
399 chunks = []
400
400
401 chunks.insert(0, [{
401 chunks.insert(0, [{
402 'old_lineno': '',
402 'old_lineno': '',
403 'new_lineno': '',
403 'new_lineno': '',
404 'action': Action.CONTEXT,
404 'action': Action.CONTEXT,
405 'line': msg,
405 'line': msg,
406 } for _op, msg in stats['ops'].iteritems()
406 } for _op, msg in stats['ops'].iteritems()
407 if _op not in [MOD_FILENODE]])
407 if _op not in [MOD_FILENODE]])
408
408
409 _files.append({
409 _files.append({
410 'filename': safe_unicode(head['b_path']),
410 'filename': safe_unicode(head['b_path']),
411 'old_revision': head['a_blob_id'],
411 'old_revision': head['a_blob_id'],
412 'new_revision': head['b_blob_id'],
412 'new_revision': head['b_blob_id'],
413 'chunks': chunks,
413 'chunks': chunks,
414 'raw_diff': safe_unicode(raw_diff),
414 'raw_diff': safe_unicode(raw_diff),
415 'operation': op,
415 'operation': op,
416 'stats': stats,
416 'stats': stats,
417 'exceeds_limit': exceeds_limit,
417 'exceeds_limit': exceeds_limit,
418 'is_limited_diff': limited_diff,
418 'is_limited_diff': limited_diff,
419 })
419 })
420
420
421 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
421 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
422 OPS.DEL: 2}.get(info['operation'])
422 OPS.DEL: 2}.get(info['operation'])
423
423
424 if not inline_diff:
424 if not inline_diff:
425 return diff_container(sorted(_files, key=sorter))
425 return diff_container(sorted(_files, key=sorter))
426
426
427 # highlight inline changes
427 # highlight inline changes
428 for diff_data in _files:
428 for diff_data in _files:
429 for chunk in diff_data['chunks']:
429 for chunk in diff_data['chunks']:
430 lineiter = iter(chunk)
430 lineiter = iter(chunk)
431 try:
431 try:
432 while 1:
432 while 1:
433 line = lineiter.next()
433 line = lineiter.next()
434 if line['action'] not in (
434 if line['action'] not in (
435 Action.UNMODIFIED, Action.CONTEXT):
435 Action.UNMODIFIED, Action.CONTEXT):
436 nextline = lineiter.next()
436 nextline = lineiter.next()
437 if nextline['action'] in ['unmod', 'context'] or \
437 if nextline['action'] in ['unmod', 'context'] or \
438 nextline['action'] == line['action']:
438 nextline['action'] == line['action']:
439 continue
439 continue
440 self.differ(line, nextline)
440 self.differ(line, nextline)
441 except StopIteration:
441 except StopIteration:
442 pass
442 pass
443
443
444 return diff_container(sorted(_files, key=sorter))
444 return diff_container(sorted(_files, key=sorter))
445
445
446 def _check_large_diff(self):
446 def _check_large_diff(self):
447 if self.diff_limit:
447 if self.diff_limit:
448 log.debug('Checking if diff exceeds current diff_limit of %s', self.diff_limit)
448 log.debug('Checking if diff exceeds current diff_limit of %s', self.diff_limit)
449 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
449 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
450 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
450 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
451
451
452 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
452 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
453 def _new_parse_gitdiff(self, inline_diff=True):
453 def _new_parse_gitdiff(self, inline_diff=True):
454 _files = []
454 _files = []
455
455
456 # this can be overriden later to a LimitedDiffContainer type
456 # this can be overriden later to a LimitedDiffContainer type
457 diff_container = lambda arg: arg
457 diff_container = lambda arg: arg
458
458
459 for chunk in self._diff.chunks():
459 for chunk in self._diff.chunks():
460 head = chunk.header
460 head = chunk.header
461 log.debug('parsing diff %r', head)
461 log.debug('parsing diff %r', head)
462
462
463 raw_diff = chunk.raw
463 raw_diff = chunk.raw
464 limited_diff = False
464 limited_diff = False
465 exceeds_limit = False
465 exceeds_limit = False
466
466
467 op = None
467 op = None
468 stats = {
468 stats = {
469 'added': 0,
469 'added': 0,
470 'deleted': 0,
470 'deleted': 0,
471 'binary': False,
471 'binary': False,
472 'old_mode': None,
472 'old_mode': None,
473 'new_mode': None,
473 'new_mode': None,
474 'ops': {},
474 'ops': {},
475 }
475 }
476 if head['old_mode']:
476 if head['old_mode']:
477 stats['old_mode'] = head['old_mode']
477 stats['old_mode'] = head['old_mode']
478 if head['new_mode']:
478 if head['new_mode']:
479 stats['new_mode'] = head['new_mode']
479 stats['new_mode'] = head['new_mode']
480 if head['b_mode']:
480 if head['b_mode']:
481 stats['new_mode'] = head['b_mode']
481 stats['new_mode'] = head['b_mode']
482
482
483 # delete file
483 # delete file
484 if head['deleted_file_mode']:
484 if head['deleted_file_mode']:
485 op = OPS.DEL
485 op = OPS.DEL
486 stats['binary'] = True
486 stats['binary'] = True
487 stats['ops'][DEL_FILENODE] = 'deleted file'
487 stats['ops'][DEL_FILENODE] = 'deleted file'
488
488
489 # new file
489 # new file
490 elif head['new_file_mode']:
490 elif head['new_file_mode']:
491 op = OPS.ADD
491 op = OPS.ADD
492 stats['binary'] = True
492 stats['binary'] = True
493 stats['old_mode'] = None
493 stats['old_mode'] = None
494 stats['new_mode'] = head['new_file_mode']
494 stats['new_mode'] = head['new_file_mode']
495 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
495 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
496
496
497 # modify operation, can be copy, rename or chmod
497 # modify operation, can be copy, rename or chmod
498 else:
498 else:
499 # CHMOD
499 # CHMOD
500 if head['new_mode'] and head['old_mode']:
500 if head['new_mode'] and head['old_mode']:
501 op = OPS.MOD
501 op = OPS.MOD
502 stats['binary'] = True
502 stats['binary'] = True
503 stats['ops'][CHMOD_FILENODE] = (
503 stats['ops'][CHMOD_FILENODE] = (
504 'modified file chmod %s => %s' % (
504 'modified file chmod %s => %s' % (
505 head['old_mode'], head['new_mode']))
505 head['old_mode'], head['new_mode']))
506
506
507 # RENAME
507 # RENAME
508 if head['rename_from'] != head['rename_to']:
508 if head['rename_from'] != head['rename_to']:
509 op = OPS.MOD
509 op = OPS.MOD
510 stats['binary'] = True
510 stats['binary'] = True
511 stats['renamed'] = (head['rename_from'], head['rename_to'])
511 stats['renamed'] = (head['rename_from'], head['rename_to'])
512 stats['ops'][RENAMED_FILENODE] = (
512 stats['ops'][RENAMED_FILENODE] = (
513 'file renamed from %s to %s' % (
513 'file renamed from %s to %s' % (
514 head['rename_from'], head['rename_to']))
514 head['rename_from'], head['rename_to']))
515 # COPY
515 # COPY
516 if head.get('copy_from') and head.get('copy_to'):
516 if head.get('copy_from') and head.get('copy_to'):
517 op = OPS.MOD
517 op = OPS.MOD
518 stats['binary'] = True
518 stats['binary'] = True
519 stats['copied'] = (head['copy_from'], head['copy_to'])
519 stats['copied'] = (head['copy_from'], head['copy_to'])
520 stats['ops'][COPIED_FILENODE] = (
520 stats['ops'][COPIED_FILENODE] = (
521 'file copied from %s to %s' % (
521 'file copied from %s to %s' % (
522 head['copy_from'], head['copy_to']))
522 head['copy_from'], head['copy_to']))
523
523
524 # If our new parsed headers didn't match anything fallback to
524 # If our new parsed headers didn't match anything fallback to
525 # old style detection
525 # old style detection
526 if op is None:
526 if op is None:
527 if not head['a_file'] and head['b_file']:
527 if not head['a_file'] and head['b_file']:
528 op = OPS.ADD
528 op = OPS.ADD
529 stats['binary'] = True
529 stats['binary'] = True
530 stats['new_file'] = True
530 stats['new_file'] = True
531 stats['ops'][NEW_FILENODE] = 'new file'
531 stats['ops'][NEW_FILENODE] = 'new file'
532
532
533 elif head['a_file'] and not head['b_file']:
533 elif head['a_file'] and not head['b_file']:
534 op = OPS.DEL
534 op = OPS.DEL
535 stats['binary'] = True
535 stats['binary'] = True
536 stats['ops'][DEL_FILENODE] = 'deleted file'
536 stats['ops'][DEL_FILENODE] = 'deleted file'
537
537
538 # it's not ADD not DELETE
538 # it's not ADD not DELETE
539 if op is None:
539 if op is None:
540 op = OPS.MOD
540 op = OPS.MOD
541 stats['binary'] = True
541 stats['binary'] = True
542 stats['ops'][MOD_FILENODE] = 'modified file'
542 stats['ops'][MOD_FILENODE] = 'modified file'
543
543
544 # a real non-binary diff
544 # a real non-binary diff
545 if head['a_file'] or head['b_file']:
545 if head['a_file'] or head['b_file']:
546 # simulate splitlines, so we keep the line end part
546 # simulate splitlines, so we keep the line end part
547 diff = self.diff_splitter(chunk.diff)
547 diff = self.diff_splitter(chunk.diff)
548
548
549 # append each file to the diff size
549 # append each file to the diff size
550 raw_chunk_size = len(raw_diff)
550 raw_chunk_size = len(raw_diff)
551
551
552 exceeds_limit = raw_chunk_size > self.file_limit
552 exceeds_limit = raw_chunk_size > self.file_limit
553 self.cur_diff_size += raw_chunk_size
553 self.cur_diff_size += raw_chunk_size
554
554
555 try:
555 try:
556 # Check each file instead of the whole diff.
556 # Check each file instead of the whole diff.
557 # Diff will hide big files but still show small ones.
557 # Diff will hide big files but still show small ones.
558 # From the tests big files are fairly safe to be parsed
558 # From the tests big files are fairly safe to be parsed
559 # but the browser is the bottleneck.
559 # but the browser is the bottleneck.
560 if not self.show_full_diff and exceeds_limit:
560 if not self.show_full_diff and exceeds_limit:
561 log.debug('File `%s` exceeds current file_limit of %s',
561 log.debug('File `%s` exceeds current file_limit of %s',
562 safe_unicode(head['b_path']), self.file_limit)
562 safe_unicode(head['b_path']), self.file_limit)
563 raise DiffLimitExceeded(
563 raise DiffLimitExceeded(
564 'File Limit %s Exceeded', self.file_limit)
564 'File Limit %s Exceeded', self.file_limit)
565
565
566 self._check_large_diff()
566 self._check_large_diff()
567
567
568 raw_diff, chunks, _stats = self._new_parse_lines(diff)
568 raw_diff, chunks, _stats = self._new_parse_lines(diff)
569 stats['binary'] = False
569 stats['binary'] = False
570 stats['added'] = _stats[0]
570 stats['added'] = _stats[0]
571 stats['deleted'] = _stats[1]
571 stats['deleted'] = _stats[1]
572 # explicit mark that it's a modified file
572 # explicit mark that it's a modified file
573 if op == OPS.MOD:
573 if op == OPS.MOD:
574 stats['ops'][MOD_FILENODE] = 'modified file'
574 stats['ops'][MOD_FILENODE] = 'modified file'
575
575
576 except DiffLimitExceeded:
576 except DiffLimitExceeded:
577 diff_container = lambda _diff: \
577 diff_container = lambda _diff: \
578 LimitedDiffContainer(
578 LimitedDiffContainer(
579 self.diff_limit, self.cur_diff_size, _diff)
579 self.diff_limit, self.cur_diff_size, _diff)
580
580
581 limited_diff = True
581 limited_diff = True
582 chunks = []
582 chunks = []
583
583
584 else: # GIT format binary patch, or possibly empty diff
584 else: # GIT format binary patch, or possibly empty diff
585 if head['bin_patch']:
585 if head['bin_patch']:
586 # we have operation already extracted, but we mark simply
586 # we have operation already extracted, but we mark simply
587 # it's a diff we wont show for binary files
587 # it's a diff we wont show for binary files
588 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
588 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
589 chunks = []
589 chunks = []
590
590
591 # Hide content of deleted node by setting empty chunks
591 # Hide content of deleted node by setting empty chunks
592 if chunks and not self.show_full_diff and op == OPS.DEL:
592 if chunks and not self.show_full_diff and op == OPS.DEL:
593 # if not full diff mode show deleted file contents
593 # if not full diff mode show deleted file contents
594 # TODO: anderson: if the view is not too big, there is no way
594 # TODO: anderson: if the view is not too big, there is no way
595 # to see the content of the file
595 # to see the content of the file
596 chunks = []
596 chunks = []
597
597
598 chunks.insert(
598 chunks.insert(
599 0, [{'old_lineno': '',
599 0, [{'old_lineno': '',
600 'new_lineno': '',
600 'new_lineno': '',
601 'action': Action.CONTEXT,
601 'action': Action.CONTEXT,
602 'line': msg,
602 'line': msg,
603 } for _op, msg in stats['ops'].iteritems()
603 } for _op, msg in stats['ops'].iteritems()
604 if _op not in [MOD_FILENODE]])
604 if _op not in [MOD_FILENODE]])
605
605
606 original_filename = safe_unicode(head['a_path'])
606 original_filename = safe_unicode(head['a_path'])
607 _files.append({
607 _files.append({
608 'original_filename': original_filename,
608 'original_filename': original_filename,
609 'filename': safe_unicode(head['b_path']),
609 'filename': safe_unicode(head['b_path']),
610 'old_revision': head['a_blob_id'],
610 'old_revision': head['a_blob_id'],
611 'new_revision': head['b_blob_id'],
611 'new_revision': head['b_blob_id'],
612 'chunks': chunks,
612 'chunks': chunks,
613 'raw_diff': safe_unicode(raw_diff),
613 'raw_diff': safe_unicode(raw_diff),
614 'operation': op,
614 'operation': op,
615 'stats': stats,
615 'stats': stats,
616 'exceeds_limit': exceeds_limit,
616 'exceeds_limit': exceeds_limit,
617 'is_limited_diff': limited_diff,
617 'is_limited_diff': limited_diff,
618 })
618 })
619
619
620 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
620 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
621 OPS.DEL: 2}.get(info['operation'])
621 OPS.DEL: 2}.get(info['operation'])
622
622
623 return diff_container(sorted(_files, key=sorter))
623 return diff_container(sorted(_files, key=sorter))
624
624
625 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
625 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
626 def _parse_lines(self, diff_iter):
626 def _parse_lines(self, diff_iter):
627 """
627 """
628 Parse the diff an return data for the template.
628 Parse the diff an return data for the template.
629 """
629 """
630
630
631 stats = [0, 0]
631 stats = [0, 0]
632 chunks = []
632 chunks = []
633 raw_diff = []
633 raw_diff = []
634
634
635 try:
635 try:
636 line = diff_iter.next()
636 line = diff_iter.next()
637
637
638 while line:
638 while line:
639 raw_diff.append(line)
639 raw_diff.append(line)
640 lines = []
640 lines = []
641 chunks.append(lines)
641 chunks.append(lines)
642
642
643 match = self._chunk_re.match(line)
643 match = self._chunk_re.match(line)
644
644
645 if not match:
645 if not match:
646 break
646 break
647
647
648 gr = match.groups()
648 gr = match.groups()
649 (old_line, old_end,
649 (old_line, old_end,
650 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
650 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
651 old_line -= 1
651 old_line -= 1
652 new_line -= 1
652 new_line -= 1
653
653
654 context = len(gr) == 5
654 context = len(gr) == 5
655 old_end += old_line
655 old_end += old_line
656 new_end += new_line
656 new_end += new_line
657
657
658 if context:
658 if context:
659 # skip context only if it's first line
659 # skip context only if it's first line
660 if int(gr[0]) > 1:
660 if int(gr[0]) > 1:
661 lines.append({
661 lines.append({
662 'old_lineno': '...',
662 'old_lineno': '...',
663 'new_lineno': '...',
663 'new_lineno': '...',
664 'action': Action.CONTEXT,
664 'action': Action.CONTEXT,
665 'line': line,
665 'line': line,
666 })
666 })
667
667
668 line = diff_iter.next()
668 line = diff_iter.next()
669
669
670 while old_line < old_end or new_line < new_end:
670 while old_line < old_end or new_line < new_end:
671 command = ' '
671 command = ' '
672 if line:
672 if line:
673 command = line[0]
673 command = line[0]
674
674
675 affects_old = affects_new = False
675 affects_old = affects_new = False
676
676
677 # ignore those if we don't expect them
677 # ignore those if we don't expect them
678 if command in '#@':
678 if command in '#@':
679 continue
679 continue
680 elif command == '+':
680 elif command == '+':
681 affects_new = True
681 affects_new = True
682 action = Action.ADD
682 action = Action.ADD
683 stats[0] += 1
683 stats[0] += 1
684 elif command == '-':
684 elif command == '-':
685 affects_old = True
685 affects_old = True
686 action = Action.DELETE
686 action = Action.DELETE
687 stats[1] += 1
687 stats[1] += 1
688 else:
688 else:
689 affects_old = affects_new = True
689 affects_old = affects_new = True
690 action = Action.UNMODIFIED
690 action = Action.UNMODIFIED
691
691
692 if not self._newline_marker.match(line):
692 if not self._newline_marker.match(line):
693 old_line += affects_old
693 old_line += affects_old
694 new_line += affects_new
694 new_line += affects_new
695 lines.append({
695 lines.append({
696 'old_lineno': affects_old and old_line or '',
696 'old_lineno': affects_old and old_line or '',
697 'new_lineno': affects_new and new_line or '',
697 'new_lineno': affects_new and new_line or '',
698 'action': action,
698 'action': action,
699 'line': self._clean_line(line, command)
699 'line': self._clean_line(line, command)
700 })
700 })
701 raw_diff.append(line)
701 raw_diff.append(line)
702
702
703 line = diff_iter.next()
703 line = diff_iter.next()
704
704
705 if self._newline_marker.match(line):
705 if self._newline_marker.match(line):
706 # we need to append to lines, since this is not
706 # we need to append to lines, since this is not
707 # counted in the line specs of diff
707 # counted in the line specs of diff
708 lines.append({
708 lines.append({
709 'old_lineno': '...',
709 'old_lineno': '...',
710 'new_lineno': '...',
710 'new_lineno': '...',
711 'action': Action.CONTEXT,
711 'action': Action.CONTEXT,
712 'line': self._clean_line(line, command)
712 'line': self._clean_line(line, command)
713 })
713 })
714
714
715 except StopIteration:
715 except StopIteration:
716 pass
716 pass
717 return ''.join(raw_diff), chunks, stats
717 return ''.join(raw_diff), chunks, stats
718
718
719 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
719 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
720 def _new_parse_lines(self, diff_iter):
720 def _new_parse_lines(self, diff_iter):
721 """
721 """
722 Parse the diff an return data for the template.
722 Parse the diff an return data for the template.
723 """
723 """
724
724
725 stats = [0, 0]
725 stats = [0, 0]
726 chunks = []
726 chunks = []
727 raw_diff = []
727 raw_diff = []
728
728
729 try:
729 try:
730 line = diff_iter.next()
730 line = diff_iter.next()
731
731
732 while line:
732 while line:
733 raw_diff.append(line)
733 raw_diff.append(line)
734 # match header e.g @@ -0,0 +1 @@\n'
734 # match header e.g @@ -0,0 +1 @@\n'
735 match = self._chunk_re.match(line)
735 match = self._chunk_re.match(line)
736
736
737 if not match:
737 if not match:
738 break
738 break
739
739
740 gr = match.groups()
740 gr = match.groups()
741 (old_line, old_end,
741 (old_line, old_end,
742 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
742 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
743
743
744 lines = []
744 lines = []
745 hunk = {
745 hunk = {
746 'section_header': gr[-1],
746 'section_header': gr[-1],
747 'source_start': old_line,
747 'source_start': old_line,
748 'source_length': old_end,
748 'source_length': old_end,
749 'target_start': new_line,
749 'target_start': new_line,
750 'target_length': new_end,
750 'target_length': new_end,
751 'lines': lines,
751 'lines': lines,
752 }
752 }
753 chunks.append(hunk)
753 chunks.append(hunk)
754
754
755 old_line -= 1
755 old_line -= 1
756 new_line -= 1
756 new_line -= 1
757
757
758 context = len(gr) == 5
758 context = len(gr) == 5
759 old_end += old_line
759 old_end += old_line
760 new_end += new_line
760 new_end += new_line
761
761
762 line = diff_iter.next()
762 line = diff_iter.next()
763
763
764 while old_line < old_end or new_line < new_end:
764 while old_line < old_end or new_line < new_end:
765 command = ' '
765 command = ' '
766 if line:
766 if line:
767 command = line[0]
767 command = line[0]
768
768
769 affects_old = affects_new = False
769 affects_old = affects_new = False
770
770
771 # ignore those if we don't expect them
771 # ignore those if we don't expect them
772 if command in '#@':
772 if command in '#@':
773 continue
773 continue
774 elif command == '+':
774 elif command == '+':
775 affects_new = True
775 affects_new = True
776 action = Action.ADD
776 action = Action.ADD
777 stats[0] += 1
777 stats[0] += 1
778 elif command == '-':
778 elif command == '-':
779 affects_old = True
779 affects_old = True
780 action = Action.DELETE
780 action = Action.DELETE
781 stats[1] += 1
781 stats[1] += 1
782 else:
782 else:
783 affects_old = affects_new = True
783 affects_old = affects_new = True
784 action = Action.UNMODIFIED
784 action = Action.UNMODIFIED
785
785
786 if not self._newline_marker.match(line):
786 if not self._newline_marker.match(line):
787 old_line += affects_old
787 old_line += affects_old
788 new_line += affects_new
788 new_line += affects_new
789 lines.append({
789 lines.append({
790 'old_lineno': affects_old and old_line or '',
790 'old_lineno': affects_old and old_line or '',
791 'new_lineno': affects_new and new_line or '',
791 'new_lineno': affects_new and new_line or '',
792 'action': action,
792 'action': action,
793 'line': self._clean_line(line, command)
793 'line': self._clean_line(line, command)
794 })
794 })
795 raw_diff.append(line)
795 raw_diff.append(line)
796
796
797 line = diff_iter.next()
797 line = diff_iter.next()
798
798
799 if self._newline_marker.match(line):
799 if self._newline_marker.match(line):
800 # we need to append to lines, since this is not
800 # we need to append to lines, since this is not
801 # counted in the line specs of diff
801 # counted in the line specs of diff
802 if affects_old:
802 if affects_old:
803 action = Action.OLD_NO_NL
803 action = Action.OLD_NO_NL
804 elif affects_new:
804 elif affects_new:
805 action = Action.NEW_NO_NL
805 action = Action.NEW_NO_NL
806 else:
806 else:
807 raise Exception('invalid context for no newline')
807 raise Exception('invalid context for no newline')
808
808
809 lines.append({
809 lines.append({
810 'old_lineno': None,
810 'old_lineno': None,
811 'new_lineno': None,
811 'new_lineno': None,
812 'action': action,
812 'action': action,
813 'line': self._clean_line(line, command)
813 'line': self._clean_line(line, command)
814 })
814 })
815
815
816 except StopIteration:
816 except StopIteration:
817 pass
817 pass
818
818
819 return ''.join(raw_diff), chunks, stats
819 return ''.join(raw_diff), chunks, stats
820
820
821 def _safe_id(self, idstring):
821 def _safe_id(self, idstring):
822 """Make a string safe for including in an id attribute.
822 """Make a string safe for including in an id attribute.
823
823
824 The HTML spec says that id attributes 'must begin with
824 The HTML spec says that id attributes 'must begin with
825 a letter ([A-Za-z]) and may be followed by any number
825 a letter ([A-Za-z]) and may be followed by any number
826 of letters, digits ([0-9]), hyphens ("-"), underscores
826 of letters, digits ([0-9]), hyphens ("-"), underscores
827 ("_"), colons (":"), and periods (".")'. These regexps
827 ("_"), colons (":"), and periods (".")'. These regexps
828 are slightly over-zealous, in that they remove colons
828 are slightly over-zealous, in that they remove colons
829 and periods unnecessarily.
829 and periods unnecessarily.
830
830
831 Whitespace is transformed into underscores, and then
831 Whitespace is transformed into underscores, and then
832 anything which is not a hyphen or a character that
832 anything which is not a hyphen or a character that
833 matches \w (alphanumerics and underscore) is removed.
833 matches \w (alphanumerics and underscore) is removed.
834
834
835 """
835 """
836 # Transform all whitespace to underscore
836 # Transform all whitespace to underscore
837 idstring = re.sub(r'\s', "_", '%s' % idstring)
837 idstring = re.sub(r'\s', "_", '%s' % idstring)
838 # Remove everything that is not a hyphen or a member of \w
838 # Remove everything that is not a hyphen or a member of \w
839 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
839 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
840 return idstring
840 return idstring
841
841
842 @classmethod
842 @classmethod
843 def diff_splitter(cls, string):
843 def diff_splitter(cls, string):
844 """
844 """
845 Diff split that emulates .splitlines() but works only on \n
845 Diff split that emulates .splitlines() but works only on \n
846 """
846 """
847 if not string:
847 if not string:
848 return
848 return
849 elif string == '\n':
849 elif string == '\n':
850 yield u'\n'
850 yield u'\n'
851 else:
851 else:
852
852
853 has_newline = string.endswith('\n')
853 has_newline = string.endswith('\n')
854 elements = string.split('\n')
854 elements = string.split('\n')
855 if has_newline:
855 if has_newline:
856 # skip last element as it's empty string from newlines
856 # skip last element as it's empty string from newlines
857 elements = elements[:-1]
857 elements = elements[:-1]
858
858
859 len_elements = len(elements)
859 len_elements = len(elements)
860
860
861 for cnt, line in enumerate(elements, start=1):
861 for cnt, line in enumerate(elements, start=1):
862 last_line = cnt == len_elements
862 last_line = cnt == len_elements
863 if last_line and not has_newline:
863 if last_line and not has_newline:
864 yield safe_unicode(line)
864 yield safe_unicode(line)
865 else:
865 else:
866 yield safe_unicode(line) + '\n'
866 yield safe_unicode(line) + '\n'
867
867
868 def prepare(self, inline_diff=True):
868 def prepare(self, inline_diff=True):
869 """
869 """
870 Prepare the passed udiff for HTML rendering.
870 Prepare the passed udiff for HTML rendering.
871
871
872 :return: A list of dicts with diff information.
872 :return: A list of dicts with diff information.
873 """
873 """
874 parsed = self._parser(inline_diff=inline_diff)
874 parsed = self._parser(inline_diff=inline_diff)
875 self.parsed = True
875 self.parsed = True
876 self.parsed_diff = parsed
876 self.parsed_diff = parsed
877 return parsed
877 return parsed
878
878
879 def as_raw(self, diff_lines=None):
879 def as_raw(self, diff_lines=None):
880 """
880 """
881 Returns raw diff as a byte string
881 Returns raw diff as a byte string
882 """
882 """
883 return self._diff.raw
883 return self._diff.raw
884
884
885 def as_html(self, table_class='code-difftable', line_class='line',
885 def as_html(self, table_class='code-difftable', line_class='line',
886 old_lineno_class='lineno old', new_lineno_class='lineno new',
886 old_lineno_class='lineno old', new_lineno_class='lineno new',
887 code_class='code', enable_comments=False, parsed_lines=None):
887 code_class='code', enable_comments=False, parsed_lines=None):
888 """
888 """
889 Return given diff as html table with customized css classes
889 Return given diff as html table with customized css classes
890 """
890 """
891 # TODO(marcink): not sure how to pass in translator
891 # TODO(marcink): not sure how to pass in translator
892 # here in an efficient way, leave the _ for proper gettext extraction
892 # here in an efficient way, leave the _ for proper gettext extraction
893 _ = lambda s: s
893 _ = lambda s: s
894
894
895 def _link_to_if(condition, label, url):
895 def _link_to_if(condition, label, url):
896 """
896 """
897 Generates a link if condition is meet or just the label if not.
897 Generates a link if condition is meet or just the label if not.
898 """
898 """
899
899
900 if condition:
900 if condition:
901 return '''<a href="%(url)s" class="tooltip"
901 return '''<a href="%(url)s" class="tooltip"
902 title="%(title)s">%(label)s</a>''' % {
902 title="%(title)s">%(label)s</a>''' % {
903 'title': _('Click to select line'),
903 'title': _('Click to select line'),
904 'url': url,
904 'url': url,
905 'label': label
905 'label': label
906 }
906 }
907 else:
907 else:
908 return label
908 return label
909 if not self.parsed:
909 if not self.parsed:
910 self.prepare()
910 self.prepare()
911
911
912 diff_lines = self.parsed_diff
912 diff_lines = self.parsed_diff
913 if parsed_lines:
913 if parsed_lines:
914 diff_lines = parsed_lines
914 diff_lines = parsed_lines
915
915
916 _html_empty = True
916 _html_empty = True
917 _html = []
917 _html = []
918 _html.append('''<table class="%(table_class)s">\n''' % {
918 _html.append('''<table class="%(table_class)s">\n''' % {
919 'table_class': table_class
919 'table_class': table_class
920 })
920 })
921
921
922 for diff in diff_lines:
922 for diff in diff_lines:
923 for line in diff['chunks']:
923 for line in diff['chunks']:
924 _html_empty = False
924 _html_empty = False
925 for change in line:
925 for change in line:
926 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
926 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
927 'lc': line_class,
927 'lc': line_class,
928 'action': change['action']
928 'action': change['action']
929 })
929 })
930 anchor_old_id = ''
930 anchor_old_id = ''
931 anchor_new_id = ''
931 anchor_new_id = ''
932 anchor_old = "%(filename)s_o%(oldline_no)s" % {
932 anchor_old = "%(filename)s_o%(oldline_no)s" % {
933 'filename': self._safe_id(diff['filename']),
933 'filename': self._safe_id(diff['filename']),
934 'oldline_no': change['old_lineno']
934 'oldline_no': change['old_lineno']
935 }
935 }
936 anchor_new = "%(filename)s_n%(oldline_no)s" % {
936 anchor_new = "%(filename)s_n%(oldline_no)s" % {
937 'filename': self._safe_id(diff['filename']),
937 'filename': self._safe_id(diff['filename']),
938 'oldline_no': change['new_lineno']
938 'oldline_no': change['new_lineno']
939 }
939 }
940 cond_old = (change['old_lineno'] != '...' and
940 cond_old = (change['old_lineno'] != '...' and
941 change['old_lineno'])
941 change['old_lineno'])
942 cond_new = (change['new_lineno'] != '...' and
942 cond_new = (change['new_lineno'] != '...' and
943 change['new_lineno'])
943 change['new_lineno'])
944 if cond_old:
944 if cond_old:
945 anchor_old_id = 'id="%s"' % anchor_old
945 anchor_old_id = 'id="%s"' % anchor_old
946 if cond_new:
946 if cond_new:
947 anchor_new_id = 'id="%s"' % anchor_new
947 anchor_new_id = 'id="%s"' % anchor_new
948
948
949 if change['action'] != Action.CONTEXT:
949 if change['action'] != Action.CONTEXT:
950 anchor_link = True
950 anchor_link = True
951 else:
951 else:
952 anchor_link = False
952 anchor_link = False
953
953
954 ###########################################################
954 ###########################################################
955 # COMMENT ICONS
955 # COMMENT ICONS
956 ###########################################################
956 ###########################################################
957 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
957 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
958
958
959 if enable_comments and change['action'] != Action.CONTEXT:
959 if enable_comments and change['action'] != Action.CONTEXT:
960 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
960 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
961
961
962 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
962 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
963
963
964 ###########################################################
964 ###########################################################
965 # OLD LINE NUMBER
965 # OLD LINE NUMBER
966 ###########################################################
966 ###########################################################
967 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
967 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
968 'a_id': anchor_old_id,
968 'a_id': anchor_old_id,
969 'olc': old_lineno_class
969 'olc': old_lineno_class
970 })
970 })
971
971
972 _html.append('''%(link)s''' % {
972 _html.append('''%(link)s''' % {
973 'link': _link_to_if(anchor_link, change['old_lineno'],
973 'link': _link_to_if(anchor_link, change['old_lineno'],
974 '#%s' % anchor_old)
974 '#%s' % anchor_old)
975 })
975 })
976 _html.append('''</td>\n''')
976 _html.append('''</td>\n''')
977 ###########################################################
977 ###########################################################
978 # NEW LINE NUMBER
978 # NEW LINE NUMBER
979 ###########################################################
979 ###########################################################
980
980
981 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
981 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
982 'a_id': anchor_new_id,
982 'a_id': anchor_new_id,
983 'nlc': new_lineno_class
983 'nlc': new_lineno_class
984 })
984 })
985
985
986 _html.append('''%(link)s''' % {
986 _html.append('''%(link)s''' % {
987 'link': _link_to_if(anchor_link, change['new_lineno'],
987 'link': _link_to_if(anchor_link, change['new_lineno'],
988 '#%s' % anchor_new)
988 '#%s' % anchor_new)
989 })
989 })
990 _html.append('''</td>\n''')
990 _html.append('''</td>\n''')
991 ###########################################################
991 ###########################################################
992 # CODE
992 # CODE
993 ###########################################################
993 ###########################################################
994 code_classes = [code_class]
994 code_classes = [code_class]
995 if (not enable_comments or
995 if (not enable_comments or
996 change['action'] == Action.CONTEXT):
996 change['action'] == Action.CONTEXT):
997 code_classes.append('no-comment')
997 code_classes.append('no-comment')
998 _html.append('\t<td class="%s">' % ' '.join(code_classes))
998 _html.append('\t<td class="%s">' % ' '.join(code_classes))
999 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
999 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
1000 'code': change['line']
1000 'code': change['line']
1001 })
1001 })
1002
1002
1003 _html.append('''\t</td>''')
1003 _html.append('''\t</td>''')
1004 _html.append('''\n</tr>\n''')
1004 _html.append('''\n</tr>\n''')
1005 _html.append('''</table>''')
1005 _html.append('''</table>''')
1006 if _html_empty:
1006 if _html_empty:
1007 return None
1007 return None
1008 return ''.join(_html)
1008 return ''.join(_html)
1009
1009
1010 def stat(self):
1010 def stat(self):
1011 """
1011 """
1012 Returns tuple of added, and removed lines for this instance
1012 Returns tuple of added, and removed lines for this instance
1013 """
1013 """
1014 return self.adds, self.removes
1014 return self.adds, self.removes
1015
1015
1016 def get_context_of_line(
1016 def get_context_of_line(
1017 self, path, diff_line=None, context_before=3, context_after=3):
1017 self, path, diff_line=None, context_before=3, context_after=3):
1018 """
1018 """
1019 Returns the context lines for the specified diff line.
1019 Returns the context lines for the specified diff line.
1020
1020
1021 :type diff_line: :class:`DiffLineNumber`
1021 :type diff_line: :class:`DiffLineNumber`
1022 """
1022 """
1023 assert self.parsed, "DiffProcessor is not initialized."
1023 assert self.parsed, "DiffProcessor is not initialized."
1024
1024
1025 if None not in diff_line:
1025 if None not in diff_line:
1026 raise ValueError(
1026 raise ValueError(
1027 "Cannot specify both line numbers: {}".format(diff_line))
1027 "Cannot specify both line numbers: {}".format(diff_line))
1028
1028
1029 file_diff = self._get_file_diff(path)
1029 file_diff = self._get_file_diff(path)
1030 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1030 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1031
1031
1032 first_line_to_include = max(idx - context_before, 0)
1032 first_line_to_include = max(idx - context_before, 0)
1033 first_line_after_context = idx + context_after + 1
1033 first_line_after_context = idx + context_after + 1
1034 context_lines = chunk[first_line_to_include:first_line_after_context]
1034 context_lines = chunk[first_line_to_include:first_line_after_context]
1035
1035
1036 line_contents = [
1036 line_contents = [
1037 _context_line(line) for line in context_lines
1037 _context_line(line) for line in context_lines
1038 if _is_diff_content(line)]
1038 if _is_diff_content(line)]
1039 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1039 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1040 # Once they are fixed, we can drop this line here.
1040 # Once they are fixed, we can drop this line here.
1041 if line_contents:
1041 if line_contents:
1042 line_contents[-1] = (
1042 line_contents[-1] = (
1043 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1043 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1044 return line_contents
1044 return line_contents
1045
1045
1046 def find_context(self, path, context, offset=0):
1046 def find_context(self, path, context, offset=0):
1047 """
1047 """
1048 Finds the given `context` inside of the diff.
1048 Finds the given `context` inside of the diff.
1049
1049
1050 Use the parameter `offset` to specify which offset the target line has
1050 Use the parameter `offset` to specify which offset the target line has
1051 inside of the given `context`. This way the correct diff line will be
1051 inside of the given `context`. This way the correct diff line will be
1052 returned.
1052 returned.
1053
1053
1054 :param offset: Shall be used to specify the offset of the main line
1054 :param offset: Shall be used to specify the offset of the main line
1055 within the given `context`.
1055 within the given `context`.
1056 """
1056 """
1057 if offset < 0 or offset >= len(context):
1057 if offset < 0 or offset >= len(context):
1058 raise ValueError(
1058 raise ValueError(
1059 "Only positive values up to the length of the context "
1059 "Only positive values up to the length of the context "
1060 "minus one are allowed.")
1060 "minus one are allowed.")
1061
1061
1062 matches = []
1062 matches = []
1063 file_diff = self._get_file_diff(path)
1063 file_diff = self._get_file_diff(path)
1064
1064
1065 for chunk in file_diff['chunks']:
1065 for chunk in file_diff['chunks']:
1066 context_iter = iter(context)
1066 context_iter = iter(context)
1067 for line_idx, line in enumerate(chunk):
1067 for line_idx, line in enumerate(chunk):
1068 try:
1068 try:
1069 if _context_line(line) == context_iter.next():
1069 if _context_line(line) == context_iter.next():
1070 continue
1070 continue
1071 except StopIteration:
1071 except StopIteration:
1072 matches.append((line_idx, chunk))
1072 matches.append((line_idx, chunk))
1073 context_iter = iter(context)
1073 context_iter = iter(context)
1074
1074
1075 # Increment position and triger StopIteration
1075 # Increment position and triger StopIteration
1076 # if we had a match at the end
1076 # if we had a match at the end
1077 line_idx += 1
1077 line_idx += 1
1078 try:
1078 try:
1079 context_iter.next()
1079 context_iter.next()
1080 except StopIteration:
1080 except StopIteration:
1081 matches.append((line_idx, chunk))
1081 matches.append((line_idx, chunk))
1082
1082
1083 effective_offset = len(context) - offset
1083 effective_offset = len(context) - offset
1084 found_at_diff_lines = [
1084 found_at_diff_lines = [
1085 _line_to_diff_line_number(chunk[idx - effective_offset])
1085 _line_to_diff_line_number(chunk[idx - effective_offset])
1086 for idx, chunk in matches]
1086 for idx, chunk in matches]
1087
1087
1088 return found_at_diff_lines
1088 return found_at_diff_lines
1089
1089
1090 def _get_file_diff(self, path):
1090 def _get_file_diff(self, path):
1091 for file_diff in self.parsed_diff:
1091 for file_diff in self.parsed_diff:
1092 if file_diff['filename'] == path:
1092 if file_diff['filename'] == path:
1093 break
1093 break
1094 else:
1094 else:
1095 raise FileNotInDiffException("File {} not in diff".format(path))
1095 raise FileNotInDiffException("File {} not in diff".format(path))
1096 return file_diff
1096 return file_diff
1097
1097
1098 def _find_chunk_line_index(self, file_diff, diff_line):
1098 def _find_chunk_line_index(self, file_diff, diff_line):
1099 for chunk in file_diff['chunks']:
1099 for chunk in file_diff['chunks']:
1100 for idx, line in enumerate(chunk):
1100 for idx, line in enumerate(chunk):
1101 if line['old_lineno'] == diff_line.old:
1101 if line['old_lineno'] == diff_line.old:
1102 return chunk, idx
1102 return chunk, idx
1103 if line['new_lineno'] == diff_line.new:
1103 if line['new_lineno'] == diff_line.new:
1104 return chunk, idx
1104 return chunk, idx
1105 raise LineNotInDiffException(
1105 raise LineNotInDiffException(
1106 "The line {} is not part of the diff.".format(diff_line))
1106 "The line {} is not part of the diff.".format(diff_line))
1107
1107
1108
1108
1109 def _is_diff_content(line):
1109 def _is_diff_content(line):
1110 return line['action'] in (
1110 return line['action'] in (
1111 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1111 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1112
1112
1113
1113
1114 def _context_line(line):
1114 def _context_line(line):
1115 return (line['action'], line['line'])
1115 return (line['action'], line['line'])
1116
1116
1117
1117
1118 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1118 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1119
1119
1120
1120
1121 def _line_to_diff_line_number(line):
1121 def _line_to_diff_line_number(line):
1122 new_line_no = line['new_lineno'] or None
1122 new_line_no = line['new_lineno'] or None
1123 old_line_no = line['old_lineno'] or None
1123 old_line_no = line['old_lineno'] or None
1124 return DiffLineNumber(old=old_line_no, new=new_line_no)
1124 return DiffLineNumber(old=old_line_no, new=new_line_no)
1125
1125
1126
1126
1127 class FileNotInDiffException(Exception):
1127 class FileNotInDiffException(Exception):
1128 """
1128 """
1129 Raised when the context for a missing file is requested.
1129 Raised when the context for a missing file is requested.
1130
1130
1131 If you request the context for a line in a file which is not part of the
1131 If you request the context for a line in a file which is not part of the
1132 given diff, then this exception is raised.
1132 given diff, then this exception is raised.
1133 """
1133 """
1134
1134
1135
1135
1136 class LineNotInDiffException(Exception):
1136 class LineNotInDiffException(Exception):
1137 """
1137 """
1138 Raised when the context for a missing line is requested.
1138 Raised when the context for a missing line is requested.
1139
1139
1140 If you request the context for a line in a file and this line is not
1140 If you request the context for a line in a file and this line is not
1141 part of the given diff, then this exception is raised.
1141 part of the given diff, then this exception is raised.
1142 """
1142 """
1143
1143
1144
1144
1145 class DiffLimitExceeded(Exception):
1145 class DiffLimitExceeded(Exception):
1146 pass
1146 pass
1147
1147
1148
1148
1149 # NOTE(marcink): if diffs.mako change, probably this
1149 # NOTE(marcink): if diffs.mako change, probably this
1150 # needs a bump to next version
1150 # needs a bump to next version
1151 CURRENT_DIFF_VERSION = 'v4'
1151 CURRENT_DIFF_VERSION = 'v5'
1152
1152
1153
1153
1154 def _cleanup_cache_file(cached_diff_file):
1154 def _cleanup_cache_file(cached_diff_file):
1155 # cleanup file to not store it "damaged"
1155 # cleanup file to not store it "damaged"
1156 try:
1156 try:
1157 os.remove(cached_diff_file)
1157 os.remove(cached_diff_file)
1158 except Exception:
1158 except Exception:
1159 log.exception('Failed to cleanup path %s', cached_diff_file)
1159 log.exception('Failed to cleanup path %s', cached_diff_file)
1160
1160
1161
1161
1162 def _get_compression_mode(cached_diff_file):
1162 def _get_compression_mode(cached_diff_file):
1163 mode = 'bz2'
1163 mode = 'bz2'
1164 if 'mode:plain' in cached_diff_file:
1164 if 'mode:plain' in cached_diff_file:
1165 mode = 'plain'
1165 mode = 'plain'
1166 elif 'mode:gzip' in cached_diff_file:
1166 elif 'mode:gzip' in cached_diff_file:
1167 mode = 'gzip'
1167 mode = 'gzip'
1168 return mode
1168 return mode
1169
1169
1170
1170
1171 def cache_diff(cached_diff_file, diff, commits):
1171 def cache_diff(cached_diff_file, diff, commits):
1172 compression_mode = _get_compression_mode(cached_diff_file)
1172 compression_mode = _get_compression_mode(cached_diff_file)
1173
1173
1174 struct = {
1174 struct = {
1175 'version': CURRENT_DIFF_VERSION,
1175 'version': CURRENT_DIFF_VERSION,
1176 'diff': diff,
1176 'diff': diff,
1177 'commits': commits
1177 'commits': commits
1178 }
1178 }
1179
1179
1180 start = time.time()
1180 start = time.time()
1181 try:
1181 try:
1182 if compression_mode == 'plain':
1182 if compression_mode == 'plain':
1183 with open(cached_diff_file, 'wb') as f:
1183 with open(cached_diff_file, 'wb') as f:
1184 pickle.dump(struct, f)
1184 pickle.dump(struct, f)
1185 elif compression_mode == 'gzip':
1185 elif compression_mode == 'gzip':
1186 with gzip.GzipFile(cached_diff_file, 'wb') as f:
1186 with gzip.GzipFile(cached_diff_file, 'wb') as f:
1187 pickle.dump(struct, f)
1187 pickle.dump(struct, f)
1188 else:
1188 else:
1189 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1189 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1190 pickle.dump(struct, f)
1190 pickle.dump(struct, f)
1191 except Exception:
1191 except Exception:
1192 log.warn('Failed to save cache', exc_info=True)
1192 log.warn('Failed to save cache', exc_info=True)
1193 _cleanup_cache_file(cached_diff_file)
1193 _cleanup_cache_file(cached_diff_file)
1194
1194
1195 log.debug('Saved diff cache under %s in %.4fs', cached_diff_file, time.time() - start)
1195 log.debug('Saved diff cache under %s in %.4fs', cached_diff_file, time.time() - start)
1196
1196
1197
1197
1198 def load_cached_diff(cached_diff_file):
1198 def load_cached_diff(cached_diff_file):
1199 compression_mode = _get_compression_mode(cached_diff_file)
1199 compression_mode = _get_compression_mode(cached_diff_file)
1200
1200
1201 default_struct = {
1201 default_struct = {
1202 'version': CURRENT_DIFF_VERSION,
1202 'version': CURRENT_DIFF_VERSION,
1203 'diff': None,
1203 'diff': None,
1204 'commits': None
1204 'commits': None
1205 }
1205 }
1206
1206
1207 has_cache = os.path.isfile(cached_diff_file)
1207 has_cache = os.path.isfile(cached_diff_file)
1208 if not has_cache:
1208 if not has_cache:
1209 log.debug('Reading diff cache file failed %s', cached_diff_file)
1209 log.debug('Reading diff cache file failed %s', cached_diff_file)
1210 return default_struct
1210 return default_struct
1211
1211
1212 data = None
1212 data = None
1213
1213
1214 start = time.time()
1214 start = time.time()
1215 try:
1215 try:
1216 if compression_mode == 'plain':
1216 if compression_mode == 'plain':
1217 with open(cached_diff_file, 'rb') as f:
1217 with open(cached_diff_file, 'rb') as f:
1218 data = pickle.load(f)
1218 data = pickle.load(f)
1219 elif compression_mode == 'gzip':
1219 elif compression_mode == 'gzip':
1220 with gzip.GzipFile(cached_diff_file, 'rb') as f:
1220 with gzip.GzipFile(cached_diff_file, 'rb') as f:
1221 data = pickle.load(f)
1221 data = pickle.load(f)
1222 else:
1222 else:
1223 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1223 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1224 data = pickle.load(f)
1224 data = pickle.load(f)
1225 except Exception:
1225 except Exception:
1226 log.warn('Failed to read diff cache file', exc_info=True)
1226 log.warn('Failed to read diff cache file', exc_info=True)
1227
1227
1228 if not data:
1228 if not data:
1229 data = default_struct
1229 data = default_struct
1230
1230
1231 if not isinstance(data, dict):
1231 if not isinstance(data, dict):
1232 # old version of data ?
1232 # old version of data ?
1233 data = default_struct
1233 data = default_struct
1234
1234
1235 # check version
1235 # check version
1236 if data.get('version') != CURRENT_DIFF_VERSION:
1236 if data.get('version') != CURRENT_DIFF_VERSION:
1237 # purge cache
1237 # purge cache
1238 _cleanup_cache_file(cached_diff_file)
1238 _cleanup_cache_file(cached_diff_file)
1239 return default_struct
1239 return default_struct
1240
1240
1241 log.debug('Loaded diff cache from %s in %.4fs', cached_diff_file, time.time() - start)
1241 log.debug('Loaded diff cache from %s in %.4fs', cached_diff_file, time.time() - start)
1242
1242
1243 return data
1243 return data
1244
1244
1245
1245
1246 def generate_diff_cache_key(*args):
1246 def generate_diff_cache_key(*args):
1247 """
1247 """
1248 Helper to generate a cache key using arguments
1248 Helper to generate a cache key using arguments
1249 """
1249 """
1250 def arg_mapper(input_param):
1250 def arg_mapper(input_param):
1251 input_param = safe_str(input_param)
1251 input_param = safe_str(input_param)
1252 # we cannot allow '/' in arguments since it would allow
1252 # we cannot allow '/' in arguments since it would allow
1253 # subdirectory usage
1253 # subdirectory usage
1254 input_param.replace('/', '_')
1254 input_param.replace('/', '_')
1255 return input_param or None # prevent empty string arguments
1255 return input_param or None # prevent empty string arguments
1256
1256
1257 return '_'.join([
1257 return '_'.join([
1258 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1258 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1259
1259
1260
1260
1261 def diff_cache_exist(cache_storage, *args):
1261 def diff_cache_exist(cache_storage, *args):
1262 """
1262 """
1263 Based on all generated arguments check and return a cache path
1263 Based on all generated arguments check and return a cache path
1264 """
1264 """
1265 args = list(args) + ['mode:gzip']
1265 args = list(args) + ['mode:gzip']
1266 cache_key = generate_diff_cache_key(*args)
1266 cache_key = generate_diff_cache_key(*args)
1267 cache_file_path = os.path.join(cache_storage, cache_key)
1267 cache_file_path = os.path.join(cache_storage, cache_key)
1268 # prevent path traversal attacks using some param that have e.g '../../'
1268 # prevent path traversal attacks using some param that have e.g '../../'
1269 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1269 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1270 raise ValueError('Final path must be within {}'.format(cache_storage))
1270 raise ValueError('Final path must be within {}'.format(cache_storage))
1271
1271
1272 return cache_file_path
1272 return cache_file_path
@@ -1,1347 +1,1347 b''
1 // Default styles
1 // Default styles
2
2
3 .diff-collapse {
3 .diff-collapse {
4 margin: @padding 0;
4 margin: @padding 0;
5 text-align: right;
5 text-align: right;
6 }
6 }
7
7
8 .diff-container {
8 .diff-container {
9 margin-bottom: @space;
9 margin-bottom: @space;
10
10
11 .diffblock {
11 .diffblock {
12 margin-bottom: @space;
12 margin-bottom: @space;
13 }
13 }
14
14
15 &.hidden {
15 &.hidden {
16 display: none;
16 display: none;
17 overflow: hidden;
17 overflow: hidden;
18 }
18 }
19 }
19 }
20
20
21
21
22 div.diffblock .sidebyside {
22 div.diffblock .sidebyside {
23 background: #ffffff;
23 background: #ffffff;
24 }
24 }
25
25
26 div.diffblock {
26 div.diffblock {
27 overflow-x: auto;
27 overflow-x: auto;
28 overflow-y: hidden;
28 overflow-y: hidden;
29 clear: both;
29 clear: both;
30 padding: 0px;
30 padding: 0px;
31 background: @grey6;
31 background: @grey6;
32 border: @border-thickness solid @grey5;
32 border: @border-thickness solid @grey5;
33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
33 -webkit-border-radius: @border-radius @border-radius 0px 0px;
34 border-radius: @border-radius @border-radius 0px 0px;
34 border-radius: @border-radius @border-radius 0px 0px;
35
35
36
36
37 .comments-number {
37 .comments-number {
38 float: right;
38 float: right;
39 }
39 }
40
40
41 // BEGIN CODE-HEADER STYLES
41 // BEGIN CODE-HEADER STYLES
42
42
43 .code-header {
43 .code-header {
44 background: @grey6;
44 background: @grey6;
45 padding: 10px 0 10px 0;
45 padding: 10px 0 10px 0;
46 height: auto;
46 height: auto;
47 width: 100%;
47 width: 100%;
48
48
49 .hash {
49 .hash {
50 float: left;
50 float: left;
51 padding: 2px 0 0 2px;
51 padding: 2px 0 0 2px;
52 }
52 }
53
53
54 .date {
54 .date {
55 float: left;
55 float: left;
56 text-transform: uppercase;
56 text-transform: uppercase;
57 padding: 4px 0px 0px 2px;
57 padding: 4px 0px 0px 2px;
58 }
58 }
59
59
60 div {
60 div {
61 margin-left: 4px;
61 margin-left: 4px;
62 }
62 }
63
63
64 div.compare_header {
64 div.compare_header {
65 min-height: 40px;
65 min-height: 40px;
66 margin: 0;
66 margin: 0;
67 padding: 0 @padding;
67 padding: 0 @padding;
68
68
69 .drop-menu {
69 .drop-menu {
70 float:left;
70 float:left;
71 display: block;
71 display: block;
72 margin:0 0 @padding 0;
72 margin:0 0 @padding 0;
73 }
73 }
74
74
75 .compare-label {
75 .compare-label {
76 float: left;
76 float: left;
77 clear: both;
77 clear: both;
78 display: inline-block;
78 display: inline-block;
79 min-width: 5em;
79 min-width: 5em;
80 margin: 0;
80 margin: 0;
81 padding: @button-padding @button-padding @button-padding 0;
81 padding: @button-padding @button-padding @button-padding 0;
82 font-weight: @text-semibold-weight;
82 font-weight: @text-semibold-weight;
83 font-family: @text-semibold;
83 font-family: @text-semibold;
84 }
84 }
85
85
86 .compare-buttons {
86 .compare-buttons {
87 float: left;
87 float: left;
88 margin: 0;
88 margin: 0;
89 padding: 0 0 @padding;
89 padding: 0 0 @padding;
90
90
91 .btn {
91 .btn {
92 margin: 0 @padding 0 0;
92 margin: 0 @padding 0 0;
93 }
93 }
94 }
94 }
95 }
95 }
96
96
97 }
97 }
98
98
99 .parents {
99 .parents {
100 float: left;
100 float: left;
101 width: 100px;
101 width: 100px;
102 font-weight: 400;
102 font-weight: 400;
103 vertical-align: middle;
103 vertical-align: middle;
104 padding: 0px 2px 0px 2px;
104 padding: 0px 2px 0px 2px;
105 background-color: @grey6;
105 background-color: @grey6;
106
106
107 #parent_link {
107 #parent_link {
108 margin: 00px 2px;
108 margin: 00px 2px;
109
109
110 &.double {
110 &.double {
111 margin: 0px 2px;
111 margin: 0px 2px;
112 }
112 }
113
113
114 &.disabled{
114 &.disabled{
115 margin-right: @padding;
115 margin-right: @padding;
116 }
116 }
117 }
117 }
118 }
118 }
119
119
120 .children {
120 .children {
121 float: right;
121 float: right;
122 width: 100px;
122 width: 100px;
123 font-weight: 400;
123 font-weight: 400;
124 vertical-align: middle;
124 vertical-align: middle;
125 text-align: right;
125 text-align: right;
126 padding: 0px 2px 0px 2px;
126 padding: 0px 2px 0px 2px;
127 background-color: @grey6;
127 background-color: @grey6;
128
128
129 #child_link {
129 #child_link {
130 margin: 0px 2px;
130 margin: 0px 2px;
131
131
132 &.double {
132 &.double {
133 margin: 0px 2px;
133 margin: 0px 2px;
134 }
134 }
135
135
136 &.disabled{
136 &.disabled{
137 margin-right: @padding;
137 margin-right: @padding;
138 }
138 }
139 }
139 }
140 }
140 }
141
141
142 .changeset_header {
142 .changeset_header {
143 height: 16px;
143 height: 16px;
144
144
145 & > div{
145 & > div{
146 margin-right: @padding;
146 margin-right: @padding;
147 }
147 }
148 }
148 }
149
149
150 .changeset_file {
150 .changeset_file {
151 text-align: left;
151 text-align: left;
152 float: left;
152 float: left;
153 padding: 0;
153 padding: 0;
154
154
155 a{
155 a{
156 display: inline-block;
156 display: inline-block;
157 margin-right: 0.5em;
157 margin-right: 0.5em;
158 }
158 }
159
159
160 #selected_mode{
160 #selected_mode{
161 margin-left: 0;
161 margin-left: 0;
162 }
162 }
163 }
163 }
164
164
165 .diff-menu-wrapper {
165 .diff-menu-wrapper {
166 float: left;
166 float: left;
167 }
167 }
168
168
169 .diff-menu {
169 .diff-menu {
170 position: absolute;
170 position: absolute;
171 background: none repeat scroll 0 0 #FFFFFF;
171 background: none repeat scroll 0 0 #FFFFFF;
172 border-color: #003367 @grey3 @grey3;
172 border-color: #003367 @grey3 @grey3;
173 border-right: 1px solid @grey3;
173 border-right: 1px solid @grey3;
174 border-style: solid solid solid;
174 border-style: solid solid solid;
175 border-width: @border-thickness;
175 border-width: @border-thickness;
176 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
176 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
177 margin-top: 5px;
177 margin-top: 5px;
178 margin-left: 1px;
178 margin-left: 1px;
179 }
179 }
180
180
181 .diff-actions, .editor-actions {
181 .diff-actions, .editor-actions {
182 float: left;
182 float: left;
183
183
184 input{
184 input{
185 margin: 0 0.5em 0 0;
185 margin: 0 0.5em 0 0;
186 }
186 }
187 }
187 }
188
188
189 // END CODE-HEADER STYLES
189 // END CODE-HEADER STYLES
190
190
191 // BEGIN CODE-BODY STYLES
191 // BEGIN CODE-BODY STYLES
192
192
193 .code-body {
193 .code-body {
194 padding: 0;
194 padding: 0;
195 background-color: #ffffff;
195 background-color: #ffffff;
196 position: relative;
196 position: relative;
197 max-width: none;
197 max-width: none;
198 box-sizing: border-box;
198 box-sizing: border-box;
199 // TODO: johbo: Parent has overflow: auto, this forces the child here
199 // TODO: johbo: Parent has overflow: auto, this forces the child here
200 // to have the intended size and to scroll. Should be simplified.
200 // to have the intended size and to scroll. Should be simplified.
201 width: 100%;
201 width: 100%;
202 overflow-x: auto;
202 overflow-x: auto;
203 }
203 }
204
204
205 pre.raw {
205 pre.raw {
206 background: white;
206 background: white;
207 color: @grey1;
207 color: @grey1;
208 }
208 }
209 // END CODE-BODY STYLES
209 // END CODE-BODY STYLES
210
210
211 }
211 }
212
212
213
213
214 table.code-difftable {
214 table.code-difftable {
215 border-collapse: collapse;
215 border-collapse: collapse;
216 width: 99%;
216 width: 99%;
217 border-radius: 0px !important;
217 border-radius: 0px !important;
218
218
219 td {
219 td {
220 padding: 0 !important;
220 padding: 0 !important;
221 background: none !important;
221 background: none !important;
222 border: 0 !important;
222 border: 0 !important;
223 }
223 }
224
224
225 .context {
225 .context {
226 background: none repeat scroll 0 0 #DDE7EF;
226 background: none repeat scroll 0 0 #DDE7EF;
227 }
227 }
228
228
229 .add {
229 .add {
230 background: none repeat scroll 0 0 #DDFFDD;
230 background: none repeat scroll 0 0 #DDFFDD;
231
231
232 ins {
232 ins {
233 background: none repeat scroll 0 0 #AAFFAA;
233 background: none repeat scroll 0 0 #AAFFAA;
234 text-decoration: none;
234 text-decoration: none;
235 }
235 }
236 }
236 }
237
237
238 .del {
238 .del {
239 background: none repeat scroll 0 0 #FFDDDD;
239 background: none repeat scroll 0 0 #FFDDDD;
240
240
241 del {
241 del {
242 background: none repeat scroll 0 0 #FFAAAA;
242 background: none repeat scroll 0 0 #FFAAAA;
243 text-decoration: none;
243 text-decoration: none;
244 }
244 }
245 }
245 }
246
246
247 /** LINE NUMBERS **/
247 /** LINE NUMBERS **/
248 .lineno {
248 .lineno {
249 padding-left: 2px !important;
249 padding-left: 2px !important;
250 padding-right: 2px;
250 padding-right: 2px;
251 text-align: right;
251 text-align: right;
252 width: 32px;
252 width: 32px;
253 -moz-user-select: none;
253 -moz-user-select: none;
254 -webkit-user-select: none;
254 -webkit-user-select: none;
255 border-right: @border-thickness solid @grey5 !important;
255 border-right: @border-thickness solid @grey5 !important;
256 border-left: 0px solid #CCC !important;
256 border-left: 0px solid #CCC !important;
257 border-top: 0px solid #CCC !important;
257 border-top: 0px solid #CCC !important;
258 border-bottom: none !important;
258 border-bottom: none !important;
259
259
260 a {
260 a {
261 &:extend(pre);
261 &:extend(pre);
262 text-align: right;
262 text-align: right;
263 padding-right: 2px;
263 padding-right: 2px;
264 cursor: pointer;
264 cursor: pointer;
265 display: block;
265 display: block;
266 width: 32px;
266 width: 32px;
267 }
267 }
268 }
268 }
269
269
270 .context {
270 .context {
271 cursor: auto;
271 cursor: auto;
272 &:extend(pre);
272 &:extend(pre);
273 }
273 }
274
274
275 .lineno-inline {
275 .lineno-inline {
276 background: none repeat scroll 0 0 #FFF !important;
276 background: none repeat scroll 0 0 #FFF !important;
277 padding-left: 2px;
277 padding-left: 2px;
278 padding-right: 2px;
278 padding-right: 2px;
279 text-align: right;
279 text-align: right;
280 width: 30px;
280 width: 30px;
281 -moz-user-select: none;
281 -moz-user-select: none;
282 -webkit-user-select: none;
282 -webkit-user-select: none;
283 }
283 }
284
284
285 /** CODE **/
285 /** CODE **/
286 .code {
286 .code {
287 display: block;
287 display: block;
288 width: 100%;
288 width: 100%;
289
289
290 td {
290 td {
291 margin: 0;
291 margin: 0;
292 padding: 0;
292 padding: 0;
293 }
293 }
294
294
295 pre {
295 pre {
296 margin: 0;
296 margin: 0;
297 padding: 0;
297 padding: 0;
298 margin-left: .5em;
298 margin-left: .5em;
299 }
299 }
300 }
300 }
301 }
301 }
302
302
303
303
304 // Comments
304 // Comments
305 .comment-selected-hl {
305 .comment-selected-hl {
306 border-left: 6px solid @comment-highlight-color !important;
306 border-left: 6px solid @comment-highlight-color !important;
307 padding-left: 3px !important;
307 padding-left: 3px !important;
308 margin-left: -7px !important;
308 margin-left: -7px !important;
309 }
309 }
310
310
311 div.comment:target,
311 div.comment:target,
312 div.comment-outdated:target {
312 div.comment-outdated:target {
313 .comment-selected-hl;
313 .comment-selected-hl;
314 }
314 }
315
315
316 //TODO: anderson: can't get an absolute number out of anything, so had to put the
316 //TODO: anderson: can't get an absolute number out of anything, so had to put the
317 //current values that might change. But to make it clear I put as a calculation
317 //current values that might change. But to make it clear I put as a calculation
318 @comment-max-width: 1065px;
318 @comment-max-width: 1065px;
319 @pr-extra-margin: 34px;
319 @pr-extra-margin: 34px;
320 @pr-border-spacing: 4px;
320 @pr-border-spacing: 4px;
321 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
321 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
322
322
323 // Pull Request
323 // Pull Request
324 .cs_files .code-difftable {
324 .cs_files .code-difftable {
325 border: @border-thickness solid @grey5; //borders only on PRs
325 border: @border-thickness solid @grey5; //borders only on PRs
326
326
327 .comment-inline-form,
327 .comment-inline-form,
328 div.comment {
328 div.comment {
329 width: @pr-comment-width;
329 width: @pr-comment-width;
330 }
330 }
331 }
331 }
332
332
333 // Changeset
333 // Changeset
334 .code-difftable {
334 .code-difftable {
335 .comment-inline-form,
335 .comment-inline-form,
336 div.comment {
336 div.comment {
337 width: @comment-max-width;
337 width: @comment-max-width;
338 }
338 }
339 }
339 }
340
340
341 //Style page
341 //Style page
342 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
342 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
343 #style-page .code-difftable{
343 #style-page .code-difftable{
344 .comment-inline-form,
344 .comment-inline-form,
345 div.comment {
345 div.comment {
346 width: @comment-max-width - @style-extra-margin;
346 width: @comment-max-width - @style-extra-margin;
347 }
347 }
348 }
348 }
349
349
350 #context-bar > h2 {
350 #context-bar > h2 {
351 font-size: 20px;
351 font-size: 20px;
352 }
352 }
353
353
354 #context-bar > h2> a {
354 #context-bar > h2> a {
355 font-size: 20px;
355 font-size: 20px;
356 }
356 }
357 // end of defaults
357 // end of defaults
358
358
359 .file_diff_buttons {
359 .file_diff_buttons {
360 padding: 0 0 @padding;
360 padding: 0 0 @padding;
361
361
362 .drop-menu {
362 .drop-menu {
363 float: left;
363 float: left;
364 margin: 0 @padding 0 0;
364 margin: 0 @padding 0 0;
365 }
365 }
366 .btn {
366 .btn {
367 margin: 0 @padding 0 0;
367 margin: 0 @padding 0 0;
368 }
368 }
369 }
369 }
370
370
371 .code-body.textarea.editor {
371 .code-body.textarea.editor {
372 max-width: none;
372 max-width: none;
373 padding: 15px;
373 padding: 15px;
374 }
374 }
375
375
376 td.injected_diff{
376 td.injected_diff{
377 max-width: 1178px;
377 max-width: 1178px;
378 overflow-x: auto;
378 overflow-x: auto;
379 overflow-y: hidden;
379 overflow-y: hidden;
380
380
381 div.diff-container,
381 div.diff-container,
382 div.diffblock{
382 div.diffblock{
383 max-width: 100%;
383 max-width: 100%;
384 }
384 }
385
385
386 div.code-body {
386 div.code-body {
387 max-width: 1124px;
387 max-width: 1124px;
388 overflow-x: auto;
388 overflow-x: auto;
389 overflow-y: hidden;
389 overflow-y: hidden;
390 padding: 0;
390 padding: 0;
391 }
391 }
392 div.diffblock {
392 div.diffblock {
393 border: none;
393 border: none;
394 }
394 }
395
395
396 &.inline-form {
396 &.inline-form {
397 width: 99%
397 width: 99%
398 }
398 }
399 }
399 }
400
400
401
401
402 table.code-difftable {
402 table.code-difftable {
403 width: 100%;
403 width: 100%;
404 }
404 }
405
405
406 /** PYGMENTS COLORING **/
406 /** PYGMENTS COLORING **/
407 div.codeblock {
407 div.codeblock {
408
408
409 // TODO: johbo: Added interim to get rid of the margin around
409 // TODO: johbo: Added interim to get rid of the margin around
410 // Select2 widgets. This needs further cleanup.
410 // Select2 widgets. This needs further cleanup.
411 overflow: auto;
411 overflow: auto;
412 padding: 0px;
412 padding: 0px;
413 border: @border-thickness solid @grey6;
413 border: @border-thickness solid @grey6;
414 .border-radius(@border-radius);
414 .border-radius(@border-radius);
415
415
416 #remove_gist {
416 #remove_gist {
417 float: right;
417 float: right;
418 }
418 }
419
419
420 .gist_url {
420 .gist_url {
421 padding: 0px 0px 35px 0px;
421 padding: 0px 0px 35px 0px;
422 }
422 }
423
423
424 .gist-desc {
424 .gist-desc {
425 clear: both;
425 clear: both;
426 margin: 0 0 10px 0;
426 margin: 0 0 10px 0;
427 code {
427 code {
428 white-space: pre-line;
428 white-space: pre-line;
429 line-height: inherit
429 line-height: inherit
430 }
430 }
431 }
431 }
432
432
433 .author {
433 .author {
434 clear: both;
434 clear: both;
435 vertical-align: middle;
435 vertical-align: middle;
436 font-weight: @text-bold-weight;
436 font-weight: @text-bold-weight;
437 font-family: @text-bold;
437 font-family: @text-bold;
438 }
438 }
439
439
440 .btn-mini {
440 .btn-mini {
441 float: left;
441 float: left;
442 margin: 0 5px 0 0;
442 margin: 0 5px 0 0;
443 }
443 }
444
444
445 .code-header {
445 .code-header {
446 padding: @padding;
446 padding: @padding;
447 border-bottom: @border-thickness solid @grey5;
447 border-bottom: @border-thickness solid @grey5;
448
448
449 .rc-user {
449 .rc-user {
450 min-width: 0;
450 min-width: 0;
451 margin-right: .5em;
451 margin-right: .5em;
452 }
452 }
453
453
454 .stats {
454 .stats {
455 clear: both;
455 clear: both;
456 margin: 0 0 @padding 0;
456 margin: 0 0 @padding 0;
457 padding: 0;
457 padding: 0;
458 .left {
458 .left {
459 float: left;
459 float: left;
460 clear: left;
460 clear: left;
461 max-width: 75%;
461 max-width: 75%;
462 margin: 0 0 @padding 0;
462 margin: 0 0 @padding 0;
463
463
464 &.item {
464 &.item {
465 margin-right: @padding;
465 margin-right: @padding;
466 &.last { border-right: none; }
466 &.last { border-right: none; }
467 }
467 }
468 }
468 }
469 .buttons { float: right; }
469 .buttons { float: right; }
470 .author {
470 .author {
471 height: 25px; margin-left: 15px; font-weight: bold;
471 height: 25px; margin-left: 15px; font-weight: bold;
472 }
472 }
473 }
473 }
474
474
475 .commit {
475 .commit {
476 margin: 5px 0 0 26px;
476 margin: 5px 0 0 26px;
477 font-weight: normal;
477 font-weight: normal;
478 white-space: pre-wrap;
478 white-space: pre-wrap;
479 }
479 }
480 }
480 }
481
481
482 .message {
482 .message {
483 position: relative;
483 position: relative;
484 margin: @padding;
484 margin: @padding;
485
485
486 .codeblock-label {
486 .codeblock-label {
487 margin: 0 0 1em 0;
487 margin: 0 0 1em 0;
488 }
488 }
489 }
489 }
490
490
491 .code-body {
491 .code-body {
492 padding: 0.8em 1em;
492 padding: 0.8em 1em;
493 background-color: #ffffff;
493 background-color: #ffffff;
494 min-width: 100%;
494 min-width: 100%;
495 box-sizing: border-box;
495 box-sizing: border-box;
496 // TODO: johbo: Parent has overflow: auto, this forces the child here
496 // TODO: johbo: Parent has overflow: auto, this forces the child here
497 // to have the intended size and to scroll. Should be simplified.
497 // to have the intended size and to scroll. Should be simplified.
498 width: 100%;
498 width: 100%;
499 overflow-x: auto;
499 overflow-x: auto;
500
500
501 img.rendered-binary {
501 img.rendered-binary {
502 height: auto;
502 height: auto;
503 width: auto;
503 width: auto;
504 }
504 }
505
505
506 .markdown-block {
506 .markdown-block {
507 padding: 1em 0;
507 padding: 1em 0;
508 }
508 }
509 }
509 }
510
510
511 .codeblock-header {
511 .codeblock-header {
512 background: @grey7;
512 background: @grey7;
513 height: 36px;
513 height: 36px;
514 }
514 }
515
515
516 .path {
516 .path {
517 border-bottom: 1px solid @grey6;
517 border-bottom: 1px solid @grey6;
518 padding: .65em 1em;
518 padding: .65em 1em;
519 height: 18px;
519 height: 18px;
520 }
520 }
521 }
521 }
522
522
523 .code-highlighttable,
523 .code-highlighttable,
524 div.codeblock {
524 div.codeblock {
525
525
526 &.readme {
526 &.readme {
527 background-color: white;
527 background-color: white;
528 }
528 }
529
529
530 .markdown-block table {
530 .markdown-block table {
531 border-collapse: collapse;
531 border-collapse: collapse;
532
532
533 th,
533 th,
534 td {
534 td {
535 padding: .5em;
535 padding: .5em;
536 border: @border-thickness solid @border-default-color;
536 border: @border-thickness solid @border-default-color;
537 }
537 }
538 }
538 }
539
539
540 table {
540 table {
541 border: 0px;
541 border: 0px;
542 margin: 0;
542 margin: 0;
543 letter-spacing: normal;
543 letter-spacing: normal;
544
544
545
545
546 td {
546 td {
547 border: 0px;
547 border: 0px;
548 vertical-align: top;
548 vertical-align: top;
549 }
549 }
550 }
550 }
551 }
551 }
552
552
553 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
553 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
554 div.search-code-body {
554 div.search-code-body {
555 background-color: #ffffff; padding: 5px 0 5px 10px;
555 background-color: #ffffff; padding: 5px 0 5px 10px;
556 pre {
556 pre {
557 .match { background-color: #faffa6;}
557 .match { background-color: #faffa6;}
558 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
558 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
559 }
559 }
560 .code-highlighttable {
560 .code-highlighttable {
561 border-collapse: collapse;
561 border-collapse: collapse;
562
562
563 tr:hover {
563 tr:hover {
564 background: #fafafa;
564 background: #fafafa;
565 }
565 }
566 td.code {
566 td.code {
567 padding-left: 10px;
567 padding-left: 10px;
568 }
568 }
569 td.line {
569 td.line {
570 border-right: 1px solid #ccc !important;
570 border-right: 1px solid #ccc !important;
571 padding-right: 10px;
571 padding-right: 10px;
572 text-align: right;
572 text-align: right;
573 font-family: @text-monospace;
573 font-family: @text-monospace;
574 span {
574 span {
575 white-space: pre-wrap;
575 white-space: pre-wrap;
576 color: #666666;
576 color: #666666;
577 }
577 }
578 }
578 }
579 }
579 }
580 }
580 }
581
581
582 div.annotatediv { margin-left: 2px; margin-right: 4px; }
582 div.annotatediv { margin-left: 2px; margin-right: 4px; }
583 .code-highlight {
583 .code-highlight {
584 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
584 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
585 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
585 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
586 pre div:target {background-color: @comment-highlight-color !important;}
586 pre div:target {background-color: @comment-highlight-color !important;}
587 }
587 }
588
588
589 .linenos a { text-decoration: none; }
589 .linenos a { text-decoration: none; }
590
590
591 .CodeMirror-selected { background: @rchighlightblue; }
591 .CodeMirror-selected { background: @rchighlightblue; }
592 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
592 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
593 .CodeMirror ::selection { background: @rchighlightblue; }
593 .CodeMirror ::selection { background: @rchighlightblue; }
594 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
594 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
595
595
596 .code { display: block; border:0px !important; }
596 .code { display: block; border:0px !important; }
597
597
598 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
598 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
599 .codehilite {
599 .codehilite {
600 /*ElasticMatch is custom RhodeCode TAG*/
600 /*ElasticMatch is custom RhodeCode TAG*/
601
601
602 .c-ElasticMatch {
602 .c-ElasticMatch {
603 background-color: #faffa6;
603 background-color: #faffa6;
604 padding: 0.2em;
604 padding: 0.2em;
605 }
605 }
606 }
606 }
607
607
608 /* This can be generated with `pygmentize -S default -f html` */
608 /* This can be generated with `pygmentize -S default -f html` */
609 .code-highlight,
609 .code-highlight,
610 .codehilite {
610 .codehilite {
611 /*ElasticMatch is custom RhodeCode TAG*/
611 /*ElasticMatch is custom RhodeCode TAG*/
612 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
612 .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
613 .hll { background-color: #ffffcc }
613 .hll { background-color: #ffffcc }
614 .c { color: #408080; font-style: italic } /* Comment */
614 .c { color: #408080; font-style: italic } /* Comment */
615 .err, .codehilite .err { border: none } /* Error */
615 .err, .codehilite .err { border: none } /* Error */
616 .k { color: #008000; font-weight: bold } /* Keyword */
616 .k { color: #008000; font-weight: bold } /* Keyword */
617 .o { color: #666666 } /* Operator */
617 .o { color: #666666 } /* Operator */
618 .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
618 .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
619 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
619 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
620 .cp { color: #BC7A00 } /* Comment.Preproc */
620 .cp { color: #BC7A00 } /* Comment.Preproc */
621 .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
621 .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
622 .c1 { color: #408080; font-style: italic } /* Comment.Single */
622 .c1 { color: #408080; font-style: italic } /* Comment.Single */
623 .cs { color: #408080; font-style: italic } /* Comment.Special */
623 .cs { color: #408080; font-style: italic } /* Comment.Special */
624 .gd { color: #A00000 } /* Generic.Deleted */
624 .gd { color: #A00000 } /* Generic.Deleted */
625 .ge { font-style: italic } /* Generic.Emph */
625 .ge { font-style: italic } /* Generic.Emph */
626 .gr { color: #FF0000 } /* Generic.Error */
626 .gr { color: #FF0000 } /* Generic.Error */
627 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
627 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
628 .gi { color: #00A000 } /* Generic.Inserted */
628 .gi { color: #00A000 } /* Generic.Inserted */
629 .go { color: #888888 } /* Generic.Output */
629 .go { color: #888888 } /* Generic.Output */
630 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
630 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
631 .gs { font-weight: bold } /* Generic.Strong */
631 .gs { font-weight: bold } /* Generic.Strong */
632 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
632 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
633 .gt { color: #0044DD } /* Generic.Traceback */
633 .gt { color: #0044DD } /* Generic.Traceback */
634 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
634 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
635 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
635 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
636 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
636 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
637 .kp { color: #008000 } /* Keyword.Pseudo */
637 .kp { color: #008000 } /* Keyword.Pseudo */
638 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
638 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
639 .kt { color: #B00040 } /* Keyword.Type */
639 .kt { color: #B00040 } /* Keyword.Type */
640 .m { color: #666666 } /* Literal.Number */
640 .m { color: #666666 } /* Literal.Number */
641 .s { color: #BA2121 } /* Literal.String */
641 .s { color: #BA2121 } /* Literal.String */
642 .na { color: #7D9029 } /* Name.Attribute */
642 .na { color: #7D9029 } /* Name.Attribute */
643 .nb { color: #008000 } /* Name.Builtin */
643 .nb { color: #008000 } /* Name.Builtin */
644 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
644 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
645 .no { color: #880000 } /* Name.Constant */
645 .no { color: #880000 } /* Name.Constant */
646 .nd { color: #AA22FF } /* Name.Decorator */
646 .nd { color: #AA22FF } /* Name.Decorator */
647 .ni { color: #999999; font-weight: bold } /* Name.Entity */
647 .ni { color: #999999; font-weight: bold } /* Name.Entity */
648 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
648 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
649 .nf { color: #0000FF } /* Name.Function */
649 .nf { color: #0000FF } /* Name.Function */
650 .nl { color: #A0A000 } /* Name.Label */
650 .nl { color: #A0A000 } /* Name.Label */
651 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
651 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
652 .nt { color: #008000; font-weight: bold } /* Name.Tag */
652 .nt { color: #008000; font-weight: bold } /* Name.Tag */
653 .nv { color: #19177C } /* Name.Variable */
653 .nv { color: #19177C } /* Name.Variable */
654 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
654 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
655 .w { color: #bbbbbb } /* Text.Whitespace */
655 .w { color: #bbbbbb } /* Text.Whitespace */
656 .mb { color: #666666 } /* Literal.Number.Bin */
656 .mb { color: #666666 } /* Literal.Number.Bin */
657 .mf { color: #666666 } /* Literal.Number.Float */
657 .mf { color: #666666 } /* Literal.Number.Float */
658 .mh { color: #666666 } /* Literal.Number.Hex */
658 .mh { color: #666666 } /* Literal.Number.Hex */
659 .mi { color: #666666 } /* Literal.Number.Integer */
659 .mi { color: #666666 } /* Literal.Number.Integer */
660 .mo { color: #666666 } /* Literal.Number.Oct */
660 .mo { color: #666666 } /* Literal.Number.Oct */
661 .sa { color: #BA2121 } /* Literal.String.Affix */
661 .sa { color: #BA2121 } /* Literal.String.Affix */
662 .sb { color: #BA2121 } /* Literal.String.Backtick */
662 .sb { color: #BA2121 } /* Literal.String.Backtick */
663 .sc { color: #BA2121 } /* Literal.String.Char */
663 .sc { color: #BA2121 } /* Literal.String.Char */
664 .dl { color: #BA2121 } /* Literal.String.Delimiter */
664 .dl { color: #BA2121 } /* Literal.String.Delimiter */
665 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
665 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
666 .s2 { color: #BA2121 } /* Literal.String.Double */
666 .s2 { color: #BA2121 } /* Literal.String.Double */
667 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
667 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
668 .sh { color: #BA2121 } /* Literal.String.Heredoc */
668 .sh { color: #BA2121 } /* Literal.String.Heredoc */
669 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
669 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
670 .sx { color: #008000 } /* Literal.String.Other */
670 .sx { color: #008000 } /* Literal.String.Other */
671 .sr { color: #BB6688 } /* Literal.String.Regex */
671 .sr { color: #BB6688 } /* Literal.String.Regex */
672 .s1 { color: #BA2121 } /* Literal.String.Single */
672 .s1 { color: #BA2121 } /* Literal.String.Single */
673 .ss { color: #19177C } /* Literal.String.Symbol */
673 .ss { color: #19177C } /* Literal.String.Symbol */
674 .bp { color: #008000 } /* Name.Builtin.Pseudo */
674 .bp { color: #008000 } /* Name.Builtin.Pseudo */
675 .fm { color: #0000FF } /* Name.Function.Magic */
675 .fm { color: #0000FF } /* Name.Function.Magic */
676 .vc { color: #19177C } /* Name.Variable.Class */
676 .vc { color: #19177C } /* Name.Variable.Class */
677 .vg { color: #19177C } /* Name.Variable.Global */
677 .vg { color: #19177C } /* Name.Variable.Global */
678 .vi { color: #19177C } /* Name.Variable.Instance */
678 .vi { color: #19177C } /* Name.Variable.Instance */
679 .vm { color: #19177C } /* Name.Variable.Magic */
679 .vm { color: #19177C } /* Name.Variable.Magic */
680 .il { color: #666666 } /* Literal.Number.Integer.Long */
680 .il { color: #666666 } /* Literal.Number.Integer.Long */
681
681
682 }
682 }
683
683
684 /* customized pre blocks for markdown/rst */
684 /* customized pre blocks for markdown/rst */
685 pre.literal-block, .codehilite pre{
685 pre.literal-block, .codehilite pre{
686 padding: @padding;
686 padding: @padding;
687 border: 1px solid @grey6;
687 border: 1px solid @grey6;
688 .border-radius(@border-radius);
688 .border-radius(@border-radius);
689 background-color: @grey7;
689 background-color: @grey7;
690 }
690 }
691
691
692
692
693 /* START NEW CODE BLOCK CSS */
693 /* START NEW CODE BLOCK CSS */
694
694
695 @cb-line-height: 18px;
695 @cb-line-height: 18px;
696 @cb-line-code-padding: 10px;
696 @cb-line-code-padding: 10px;
697 @cb-text-padding: 5px;
697 @cb-text-padding: 5px;
698
698
699 @pill-padding: 2px 7px;
699 @pill-padding: 2px 7px;
700 @pill-padding-small: 2px 2px 1px 2px;
700 @pill-padding-small: 2px 2px 1px 2px;
701
701
702 input.filediff-collapse-state {
702 input.filediff-collapse-state {
703 display: none;
703 display: none;
704
704
705 &:checked + .filediff { /* file diff is collapsed */
705 &:checked + .filediff { /* file diff is collapsed */
706 .cb {
706 .cb {
707 display: none
707 display: none
708 }
708 }
709 .filediff-collapse-indicator {
709 .filediff-collapse-indicator {
710 float: left;
710 float: left;
711 cursor: pointer;
711 cursor: pointer;
712 margin: 1px -5px;
712 margin: 1px -5px;
713 }
713 }
714 .filediff-collapse-indicator:before {
714 .filediff-collapse-indicator:before {
715 content: '\f105';
715 content: '\f105';
716 }
716 }
717
717
718 .filediff-menu {
718 .filediff-menu {
719 display: none;
719 display: none;
720 }
720 }
721
721
722 }
722 }
723
723
724 &+ .filediff { /* file diff is expanded */
724 &+ .filediff { /* file diff is expanded */
725
725
726 .filediff-collapse-indicator {
726 .filediff-collapse-indicator {
727 float: left;
727 float: left;
728 cursor: pointer;
728 cursor: pointer;
729 margin: 1px -5px;
729 margin: 1px -5px;
730 }
730 }
731 .filediff-collapse-indicator:before {
731 .filediff-collapse-indicator:before {
732 content: '\f107';
732 content: '\f107';
733 }
733 }
734
734
735 .filediff-menu {
735 .filediff-menu {
736 display: block;
736 display: block;
737 }
737 }
738
738
739 margin: 10px 0;
739 margin: 10px 0;
740 &:nth-child(2) {
740 &:nth-child(2) {
741 margin: 0;
741 margin: 0;
742 }
742 }
743 }
743 }
744 }
744 }
745
745
746 .filediffs .anchor {
746 .filediffs .anchor {
747 display: block;
747 display: block;
748 height: 40px;
748 height: 40px;
749 margin-top: -40px;
749 margin-top: -40px;
750 visibility: hidden;
750 visibility: hidden;
751 }
751 }
752
752
753 .filediffs .anchor:nth-of-type(1) {
753 .filediffs .anchor:nth-of-type(1) {
754 display: block;
754 display: block;
755 height: 80px;
755 height: 80px;
756 margin-top: -80px;
756 margin-top: -80px;
757 visibility: hidden;
757 visibility: hidden;
758 }
758 }
759
759
760 .cs_files {
760 .cs_files {
761 clear: both;
761 clear: both;
762 }
762 }
763
763
764 #diff-file-sticky{
764 #diff-file-sticky{
765 will-change: min-height;
765 will-change: min-height;
766 height: 80px;
766 height: 80px;
767 }
767 }
768
768
769 .sidebar__inner{
769 .sidebar__inner{
770 transform: translate(0, 0); /* For browsers don't support translate3d. */
770 transform: translate(0, 0); /* For browsers don't support translate3d. */
771 transform: translate3d(0, 0, 0);
771 transform: translate3d(0, 0, 0);
772 will-change: position, transform;
772 will-change: position, transform;
773 height: 65px;
773 height: 65px;
774 background-color: #fff;
774 background-color: #fff;
775 padding: 5px 0px;
775 padding: 5px 0px;
776 }
776 }
777
777
778 .sidebar__bar {
778 .sidebar__bar {
779 padding: 5px 0px 0px 0px
779 padding: 5px 0px 0px 0px
780 }
780 }
781
781
782 .fpath-placeholder {
782 .fpath-placeholder {
783 clear: both;
783 clear: both;
784 visibility: hidden
784 visibility: hidden
785 }
785 }
786
786
787 .is-affixed {
787 .is-affixed {
788
788
789 .sidebar__inner {
789 .sidebar__inner {
790 z-index: 30;
790 z-index: 30;
791 }
791 }
792
792
793 .sidebar_inner_shadow {
793 .sidebar_inner_shadow {
794 position: fixed;
794 position: fixed;
795 top: 75px;
795 top: 75px;
796 right: -100%;
796 right: -100%;
797 left: -100%;
797 left: -100%;
798 z-index: 30;
798 z-index: 30;
799 display: block;
799 display: block;
800 height: 5px;
800 height: 5px;
801 content: "";
801 content: "";
802 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
802 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
803 border-top: 1px solid rgba(0, 0, 0, 0.15);
803 border-top: 1px solid rgba(0, 0, 0, 0.15);
804 }
804 }
805
805
806 .fpath-placeholder {
806 .fpath-placeholder {
807 visibility: visible !important;
807 visibility: visible !important;
808 }
808 }
809 }
809 }
810
810
811 .diffset-menu {
811 .diffset-menu {
812
812
813 }
813 }
814
814
815 #todo-box {
815 #todo-box {
816 clear:both;
816 clear:both;
817 display: none;
817 display: none;
818 text-align: right
818 text-align: right
819 }
819 }
820
820
821 .diffset {
821 .diffset {
822 margin: 0px auto;
822 margin: 0px auto;
823 .diffset-heading {
823 .diffset-heading {
824 border: 1px solid @grey5;
824 border: 1px solid @grey5;
825 margin-bottom: -1px;
825 margin-bottom: -1px;
826 // margin-top: 20px;
826 // margin-top: 20px;
827 h2 {
827 h2 {
828 margin: 0;
828 margin: 0;
829 line-height: 38px;
829 line-height: 38px;
830 padding-left: 10px;
830 padding-left: 10px;
831 }
831 }
832 .btn {
832 .btn {
833 margin: 0;
833 margin: 0;
834 }
834 }
835 background: @grey6;
835 background: @grey6;
836 display: block;
836 display: block;
837 padding: 5px;
837 padding: 5px;
838 }
838 }
839 .diffset-heading-warning {
839 .diffset-heading-warning {
840 background: @alert3-inner;
840 background: @alert3-inner;
841 border: 1px solid @alert3;
841 border: 1px solid @alert3;
842 }
842 }
843 &.diffset-comments-disabled {
843 &.diffset-comments-disabled {
844 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
844 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
845 display: none !important;
845 display: none !important;
846 }
846 }
847 }
847 }
848 }
848 }
849
849
850 .filelist {
850 .filelist {
851 .pill {
851 .pill {
852 display: block;
852 display: block;
853 float: left;
853 float: left;
854 padding: @pill-padding-small;
854 padding: @pill-padding-small;
855 }
855 }
856 }
856 }
857
857
858 .pill {
858 .pill {
859 display: block;
859 display: block;
860 float: left;
860 float: left;
861 padding: @pill-padding;
861 padding: @pill-padding;
862 }
862 }
863
863
864 .pill-group {
864 .pill-group {
865 .pill {
865 .pill {
866 opacity: .8;
866 opacity: .8;
867 margin-right: 3px;
867 margin-right: 3px;
868 font-size: 12px;
868 font-size: 12px;
869 font-weight: normal;
869 font-weight: normal;
870 min-width: 30px;
870 min-width: 30px;
871 text-align: center;
871 text-align: center;
872
872
873 &:first-child {
873 &:first-child {
874 border-radius: @border-radius 0 0 @border-radius;
874 border-radius: @border-radius 0 0 @border-radius;
875 }
875 }
876 &:last-child {
876 &:last-child {
877 border-radius: 0 @border-radius @border-radius 0;
877 border-radius: 0 @border-radius @border-radius 0;
878 }
878 }
879 &:only-child {
879 &:only-child {
880 border-radius: @border-radius;
880 border-radius: @border-radius;
881 margin-right: 0;
881 margin-right: 0;
882 }
882 }
883 }
883 }
884 }
884 }
885
885
886 /* Main comments*/
886 /* Main comments*/
887 #comments {
887 #comments {
888 .comment-selected {
888 .comment-selected {
889 border-left: 6px solid @comment-highlight-color;
889 border-left: 6px solid @comment-highlight-color;
890 padding-left: 3px;
890 padding-left: 3px;
891 margin-left: -9px;
891 margin-left: -9px;
892 }
892 }
893 }
893 }
894
894
895 .filediff {
895 .filediff {
896 border: 1px solid @grey5;
896 border: 1px solid @grey5;
897
897
898 /* START OVERRIDES */
898 /* START OVERRIDES */
899 .code-highlight {
899 .code-highlight {
900 border: none; // TODO: remove this border from the global
900 border: none; // TODO: remove this border from the global
901 // .code-highlight, it doesn't belong there
901 // .code-highlight, it doesn't belong there
902 }
902 }
903 label {
903 label {
904 margin: 0; // TODO: remove this margin definition from global label
904 margin: 0; // TODO: remove this margin definition from global label
905 // it doesn't belong there - if margin on labels
905 // it doesn't belong there - if margin on labels
906 // are needed for a form they should be defined
906 // are needed for a form they should be defined
907 // in the form's class
907 // in the form's class
908 }
908 }
909 /* END OVERRIDES */
909 /* END OVERRIDES */
910
910
911 * {
911 * {
912 box-sizing: border-box;
912 box-sizing: border-box;
913 }
913 }
914
914
915 .on-hover-icon {
915 .on-hover-icon {
916 visibility: hidden;
916 visibility: hidden;
917 }
917 }
918
918
919 .filediff-anchor {
919 .filediff-anchor {
920 visibility: hidden;
920 visibility: hidden;
921 }
921 }
922 &:hover {
922 &:hover {
923 .filediff-anchor {
923 .filediff-anchor {
924 visibility: visible;
924 visibility: visible;
925 }
925 }
926 .on-hover-icon {
926 .on-hover-icon {
927 visibility: visible;
927 visibility: visible;
928 }
928 }
929 }
929 }
930
930
931 .filediff-heading {
931 .filediff-heading {
932 cursor: pointer;
932 cursor: pointer;
933 display: block;
933 display: block;
934 padding: 10px 10px;
934 padding: 10px 10px;
935 }
935 }
936 .filediff-heading:after {
936 .filediff-heading:after {
937 content: "";
937 content: "";
938 display: table;
938 display: table;
939 clear: both;
939 clear: both;
940 }
940 }
941 .filediff-heading:hover {
941 .filediff-heading:hover {
942 background: #e1e9f4 !important;
942 background: #e1e9f4 !important;
943 }
943 }
944
944
945 .filediff-menu {
945 .filediff-menu {
946 text-align: right;
946 text-align: right;
947 padding: 5px 5px 5px 0px;
947 padding: 5px 5px 5px 0px;
948 background: @grey7;
948 background: @grey7;
949
949
950 &> a,
950 &> a,
951 &> span {
951 &> span {
952 padding: 1px;
952 padding: 1px;
953 }
953 }
954 }
954 }
955
955
956 .filediff-collapse-button, .filediff-expand-button {
956 .filediff-collapse-button, .filediff-expand-button {
957 cursor: pointer;
957 cursor: pointer;
958 }
958 }
959 .filediff-collapse-button {
959 .filediff-collapse-button {
960 display: inline;
960 display: inline;
961 }
961 }
962 .filediff-expand-button {
962 .filediff-expand-button {
963 display: none;
963 display: none;
964 }
964 }
965 .filediff-collapsed .filediff-collapse-button {
965 .filediff-collapsed .filediff-collapse-button {
966 display: none;
966 display: none;
967 }
967 }
968 .filediff-collapsed .filediff-expand-button {
968 .filediff-collapsed .filediff-expand-button {
969 display: inline;
969 display: inline;
970 }
970 }
971
971
972 /**** COMMENTS ****/
972 /**** COMMENTS ****/
973
973
974 .filediff-menu {
974 .filediff-menu {
975 .show-comment-button {
975 .show-comment-button {
976 display: none;
976 display: none;
977 }
977 }
978 }
978 }
979 &.hide-comments {
979 &.hide-comments {
980 .inline-comments {
980 .inline-comments {
981 display: none;
981 display: none;
982 }
982 }
983 .filediff-menu {
983 .filediff-menu {
984 .show-comment-button {
984 .show-comment-button {
985 display: inline;
985 display: inline;
986 }
986 }
987 .hide-comment-button {
987 .hide-comment-button {
988 display: none;
988 display: none;
989 }
989 }
990 }
990 }
991 }
991 }
992
992
993 .hide-line-comments {
993 .hide-line-comments {
994 .inline-comments {
994 .inline-comments {
995 display: none;
995 display: none;
996 }
996 }
997 }
997 }
998
998
999 /**** END COMMENTS ****/
999 /**** END COMMENTS ****/
1000
1000
1001
1001
1002 .nav-chunk {
1002 .nav-chunk {
1003 position: absolute;
1003 position: absolute;
1004 right: 20px;
1004 right: 20px;
1005 margin-top: -17px;
1005 margin-top: -15px;
1006 }
1006 }
1007
1007
1008 .nav-chunk.selected {
1008 .nav-chunk.selected {
1009 visibility: visible !important;
1009 visibility: visible !important;
1010 }
1010 }
1011
1011
1012 #diff_nav {
1012 #diff_nav {
1013 color: @grey3;
1013 color: @grey3;
1014 }
1014 }
1015
1015
1016 }
1016 }
1017
1017
1018
1018
1019 .op-added {
1019 .op-added {
1020 color: @alert1;
1020 color: @alert1;
1021 }
1021 }
1022
1022
1023 .op-deleted {
1023 .op-deleted {
1024 color: @alert2;
1024 color: @alert2;
1025 }
1025 }
1026
1026
1027 .filediff, .filelist {
1027 .filediff, .filelist {
1028
1028
1029 .pill {
1029 .pill {
1030 &[op="name"] {
1030 &[op="name"] {
1031 background: none;
1031 background: none;
1032 opacity: 1;
1032 opacity: 1;
1033 color: white;
1033 color: white;
1034 }
1034 }
1035 &[op="limited"] {
1035 &[op="limited"] {
1036 background: @grey2;
1036 background: @grey2;
1037 color: white;
1037 color: white;
1038 }
1038 }
1039 &[op="binary"] {
1039 &[op="binary"] {
1040 background: @color7;
1040 background: @color7;
1041 color: white;
1041 color: white;
1042 }
1042 }
1043 &[op="modified"] {
1043 &[op="modified"] {
1044 background: @alert1;
1044 background: @alert1;
1045 color: white;
1045 color: white;
1046 }
1046 }
1047 &[op="renamed"] {
1047 &[op="renamed"] {
1048 background: @color4;
1048 background: @color4;
1049 color: white;
1049 color: white;
1050 }
1050 }
1051 &[op="copied"] {
1051 &[op="copied"] {
1052 background: @color4;
1052 background: @color4;
1053 color: white;
1053 color: white;
1054 }
1054 }
1055 &[op="mode"] {
1055 &[op="mode"] {
1056 background: @grey3;
1056 background: @grey3;
1057 color: white;
1057 color: white;
1058 }
1058 }
1059 &[op="symlink"] {
1059 &[op="symlink"] {
1060 background: @color8;
1060 background: @color8;
1061 color: white;
1061 color: white;
1062 }
1062 }
1063
1063
1064 &[op="added"] { /* added lines */
1064 &[op="added"] { /* added lines */
1065 background: @alert1;
1065 background: @alert1;
1066 color: white;
1066 color: white;
1067 }
1067 }
1068 &[op="deleted"] { /* deleted lines */
1068 &[op="deleted"] { /* deleted lines */
1069 background: @alert2;
1069 background: @alert2;
1070 color: white;
1070 color: white;
1071 }
1071 }
1072
1072
1073 &[op="created"] { /* created file */
1073 &[op="created"] { /* created file */
1074 background: @alert1;
1074 background: @alert1;
1075 color: white;
1075 color: white;
1076 }
1076 }
1077 &[op="removed"] { /* deleted file */
1077 &[op="removed"] { /* deleted file */
1078 background: @color5;
1078 background: @color5;
1079 color: white;
1079 color: white;
1080 }
1080 }
1081
1081
1082 &[op="comments"] { /* comments on file */
1082 &[op="comments"] { /* comments on file */
1083 background: @grey4;
1083 background: @grey4;
1084 color: white;
1084 color: white;
1085 }
1085 }
1086
1086
1087 &[op="options"] { /* context menu */
1087 &[op="options"] { /* context menu */
1088 background: @grey6;
1088 background: @grey6;
1089 color: black;
1089 color: black;
1090 }
1090 }
1091 }
1091 }
1092 }
1092 }
1093
1093
1094
1094
1095 .filediff-outdated {
1095 .filediff-outdated {
1096 padding: 8px 0;
1096 padding: 8px 0;
1097
1097
1098 .filediff-heading {
1098 .filediff-heading {
1099 opacity: .5;
1099 opacity: .5;
1100 }
1100 }
1101 }
1101 }
1102
1102
1103 table.cb {
1103 table.cb {
1104 width: 100%;
1104 width: 100%;
1105 border-collapse: collapse;
1105 border-collapse: collapse;
1106
1106
1107 .cb-text {
1107 .cb-text {
1108 padding: @cb-text-padding;
1108 padding: @cb-text-padding;
1109 }
1109 }
1110 .cb-hunk {
1110 .cb-hunk {
1111 padding: @cb-text-padding;
1111 padding: @cb-text-padding;
1112 }
1112 }
1113 .cb-expand {
1113 .cb-expand {
1114 display: none;
1114 display: none;
1115 }
1115 }
1116 .cb-collapse {
1116 .cb-collapse {
1117 display: inline;
1117 display: inline;
1118 }
1118 }
1119 &.cb-collapsed {
1119 &.cb-collapsed {
1120 .cb-line {
1120 .cb-line {
1121 display: none;
1121 display: none;
1122 }
1122 }
1123 .cb-expand {
1123 .cb-expand {
1124 display: inline;
1124 display: inline;
1125 }
1125 }
1126 .cb-collapse {
1126 .cb-collapse {
1127 display: none;
1127 display: none;
1128 }
1128 }
1129 .cb-hunk {
1129 .cb-hunk {
1130 display: none;
1130 display: none;
1131 }
1131 }
1132 }
1132 }
1133
1133
1134 /* intentionally general selector since .cb-line-selected must override it
1134 /* intentionally general selector since .cb-line-selected must override it
1135 and they both use !important since the td itself may have a random color
1135 and they both use !important since the td itself may have a random color
1136 generated by annotation blocks. TLDR: if you change it, make sure
1136 generated by annotation blocks. TLDR: if you change it, make sure
1137 annotated block selection and line selection in file view still work */
1137 annotated block selection and line selection in file view still work */
1138 .cb-line-fresh .cb-content {
1138 .cb-line-fresh .cb-content {
1139 background: white !important;
1139 background: white !important;
1140 }
1140 }
1141 .cb-warning {
1141 .cb-warning {
1142 background: #fff4dd;
1142 background: #fff4dd;
1143 }
1143 }
1144
1144
1145 &.cb-diff-sideside {
1145 &.cb-diff-sideside {
1146 td {
1146 td {
1147 &.cb-content {
1147 &.cb-content {
1148 width: 50%;
1148 width: 50%;
1149 }
1149 }
1150 }
1150 }
1151 }
1151 }
1152
1152
1153 tr {
1153 tr {
1154 &.cb-annotate {
1154 &.cb-annotate {
1155 border-top: 1px solid #eee;
1155 border-top: 1px solid #eee;
1156 }
1156 }
1157
1157
1158 &.cb-comment-info {
1158 &.cb-comment-info {
1159 border-top: 1px solid #eee;
1159 border-top: 1px solid #eee;
1160 color: rgba(0, 0, 0, 0.3);
1160 color: rgba(0, 0, 0, 0.3);
1161 background: #edf2f9;
1161 background: #edf2f9;
1162
1162
1163 td {
1163 td {
1164
1164
1165 }
1165 }
1166 }
1166 }
1167
1167
1168 &.cb-hunk {
1168 &.cb-hunk {
1169 font-family: @text-monospace;
1169 font-family: @text-monospace;
1170 color: rgba(0, 0, 0, 0.3);
1170 color: rgba(0, 0, 0, 0.3);
1171
1171
1172 td {
1172 td {
1173 &:first-child {
1173 &:first-child {
1174 background: #edf2f9;
1174 background: #edf2f9;
1175 }
1175 }
1176 &:last-child {
1176 &:last-child {
1177 background: #f4f7fb;
1177 background: #f4f7fb;
1178 }
1178 }
1179 }
1179 }
1180 }
1180 }
1181 }
1181 }
1182
1182
1183
1183
1184 td {
1184 td {
1185 vertical-align: top;
1185 vertical-align: top;
1186 padding: 0;
1186 padding: 0;
1187
1187
1188 &.cb-content {
1188 &.cb-content {
1189 font-size: 12.35px;
1189 font-size: 12.35px;
1190
1190
1191 &.cb-line-selected .cb-code {
1191 &.cb-line-selected .cb-code {
1192 background: @comment-highlight-color !important;
1192 background: @comment-highlight-color !important;
1193 }
1193 }
1194
1194
1195 span.cb-code {
1195 span.cb-code {
1196 line-height: @cb-line-height;
1196 line-height: @cb-line-height;
1197 padding-left: @cb-line-code-padding;
1197 padding-left: @cb-line-code-padding;
1198 padding-right: @cb-line-code-padding;
1198 padding-right: @cb-line-code-padding;
1199 display: block;
1199 display: block;
1200 white-space: pre-wrap;
1200 white-space: pre-wrap;
1201 font-family: @text-monospace;
1201 font-family: @text-monospace;
1202 word-break: break-all;
1202 word-break: break-all;
1203 .nonl {
1203 .nonl {
1204 color: @color5;
1204 color: @color5;
1205 }
1205 }
1206 .cb-action {
1206 .cb-action {
1207 &:before {
1207 &:before {
1208 content: " ";
1208 content: " ";
1209 }
1209 }
1210 &.cb-deletion:before {
1210 &.cb-deletion:before {
1211 content: "- ";
1211 content: "- ";
1212 }
1212 }
1213 &.cb-addition:before {
1213 &.cb-addition:before {
1214 content: "+ ";
1214 content: "+ ";
1215 }
1215 }
1216 }
1216 }
1217 }
1217 }
1218
1218
1219 &> button.cb-comment-box-opener {
1219 &> button.cb-comment-box-opener {
1220
1220
1221 padding: 2px 2px 1px 3px;
1221 padding: 2px 2px 1px 3px;
1222 margin-left: -6px;
1222 margin-left: -6px;
1223 margin-top: -1px;
1223 margin-top: -1px;
1224
1224
1225 border-radius: @border-radius;
1225 border-radius: @border-radius;
1226 position: absolute;
1226 position: absolute;
1227 display: none;
1227 display: none;
1228 }
1228 }
1229 .cb-comment {
1229 .cb-comment {
1230 margin-top: 10px;
1230 margin-top: 10px;
1231 white-space: normal;
1231 white-space: normal;
1232 }
1232 }
1233 }
1233 }
1234 &:hover {
1234 &:hover {
1235 button.cb-comment-box-opener {
1235 button.cb-comment-box-opener {
1236 display: block;
1236 display: block;
1237 }
1237 }
1238 &+ td button.cb-comment-box-opener {
1238 &+ td button.cb-comment-box-opener {
1239 display: block
1239 display: block
1240 }
1240 }
1241 }
1241 }
1242
1242
1243 &.cb-data {
1243 &.cb-data {
1244 text-align: right;
1244 text-align: right;
1245 width: 30px;
1245 width: 30px;
1246 font-family: @text-monospace;
1246 font-family: @text-monospace;
1247
1247
1248 .icon-comment {
1248 .icon-comment {
1249 cursor: pointer;
1249 cursor: pointer;
1250 }
1250 }
1251 &.cb-line-selected {
1251 &.cb-line-selected {
1252 background: @comment-highlight-color !important;
1252 background: @comment-highlight-color !important;
1253 }
1253 }
1254 &.cb-line-selected > div {
1254 &.cb-line-selected > div {
1255 display: block;
1255 display: block;
1256 background: @comment-highlight-color !important;
1256 background: @comment-highlight-color !important;
1257 line-height: @cb-line-height;
1257 line-height: @cb-line-height;
1258 color: rgba(0, 0, 0, 0.3);
1258 color: rgba(0, 0, 0, 0.3);
1259 }
1259 }
1260 }
1260 }
1261
1261
1262 &.cb-lineno {
1262 &.cb-lineno {
1263 padding: 0;
1263 padding: 0;
1264 width: 50px;
1264 width: 50px;
1265 color: rgba(0, 0, 0, 0.3);
1265 color: rgba(0, 0, 0, 0.3);
1266 text-align: right;
1266 text-align: right;
1267 border-right: 1px solid #eee;
1267 border-right: 1px solid #eee;
1268 font-family: @text-monospace;
1268 font-family: @text-monospace;
1269 -webkit-user-select: none;
1269 -webkit-user-select: none;
1270 -moz-user-select: none;
1270 -moz-user-select: none;
1271 user-select: none;
1271 user-select: none;
1272
1272
1273 a::before {
1273 a::before {
1274 content: attr(data-line-no);
1274 content: attr(data-line-no);
1275 }
1275 }
1276 &.cb-line-selected {
1276 &.cb-line-selected {
1277 background: @comment-highlight-color !important;
1277 background: @comment-highlight-color !important;
1278 }
1278 }
1279
1279
1280 a {
1280 a {
1281 display: block;
1281 display: block;
1282 padding-right: @cb-line-code-padding;
1282 padding-right: @cb-line-code-padding;
1283 padding-left: @cb-line-code-padding;
1283 padding-left: @cb-line-code-padding;
1284 line-height: @cb-line-height;
1284 line-height: @cb-line-height;
1285 color: rgba(0, 0, 0, 0.3);
1285 color: rgba(0, 0, 0, 0.3);
1286 }
1286 }
1287 }
1287 }
1288
1288
1289 &.cb-empty {
1289 &.cb-empty {
1290 background: @grey7;
1290 background: @grey7;
1291 }
1291 }
1292
1292
1293 ins {
1293 ins {
1294 color: black;
1294 color: black;
1295 background: #a6f3a6;
1295 background: #a6f3a6;
1296 text-decoration: none;
1296 text-decoration: none;
1297 }
1297 }
1298 del {
1298 del {
1299 color: black;
1299 color: black;
1300 background: #f8cbcb;
1300 background: #f8cbcb;
1301 text-decoration: none;
1301 text-decoration: none;
1302 }
1302 }
1303 &.cb-addition {
1303 &.cb-addition {
1304 background: #ecffec;
1304 background: #ecffec;
1305
1305
1306 &.blob-lineno {
1306 &.blob-lineno {
1307 background: #ddffdd;
1307 background: #ddffdd;
1308 }
1308 }
1309 }
1309 }
1310 &.cb-deletion {
1310 &.cb-deletion {
1311 background: #ffecec;
1311 background: #ffecec;
1312
1312
1313 &.blob-lineno {
1313 &.blob-lineno {
1314 background: #ffdddd;
1314 background: #ffdddd;
1315 }
1315 }
1316 }
1316 }
1317 &.cb-annotate-message-spacer {
1317 &.cb-annotate-message-spacer {
1318 width:8px;
1318 width:8px;
1319 padding: 1px 0px 0px 3px;
1319 padding: 1px 0px 0px 3px;
1320 }
1320 }
1321 &.cb-annotate-info {
1321 &.cb-annotate-info {
1322 width: 320px;
1322 width: 320px;
1323 min-width: 320px;
1323 min-width: 320px;
1324 max-width: 320px;
1324 max-width: 320px;
1325 padding: 5px 2px;
1325 padding: 5px 2px;
1326 font-size: 13px;
1326 font-size: 13px;
1327
1327
1328 .cb-annotate-message {
1328 .cb-annotate-message {
1329 padding: 2px 0px 0px 0px;
1329 padding: 2px 0px 0px 0px;
1330 white-space: pre-line;
1330 white-space: pre-line;
1331 overflow: hidden;
1331 overflow: hidden;
1332 }
1332 }
1333 .rc-user {
1333 .rc-user {
1334 float: none;
1334 float: none;
1335 padding: 0 6px 0 17px;
1335 padding: 0 6px 0 17px;
1336 min-width: unset;
1336 min-width: unset;
1337 min-height: unset;
1337 min-height: unset;
1338 }
1338 }
1339 }
1339 }
1340
1340
1341 &.cb-annotate-revision {
1341 &.cb-annotate-revision {
1342 cursor: pointer;
1342 cursor: pointer;
1343 text-align: right;
1343 text-align: right;
1344 padding: 1px 3px 0px 3px;
1344 padding: 1px 3px 0px 3px;
1345 }
1345 }
1346 }
1346 }
1347 }
1347 }
@@ -1,642 +1,746 b''
1 // comments.less
1 // comments.less
2 // For use in RhodeCode applications;
2 // For use in RhodeCode applications;
3 // see style guide documentation for guidelines.
3 // see style guide documentation for guidelines.
4
4
5
5
6 // Comments
6 // Comments
7 @comment-outdated-opacity: 0.6;
7 @comment-outdated-opacity: 1.0;
8
8
9 .comments {
9 .comments {
10 width: 100%;
10 width: 100%;
11 }
11 }
12
12
13 .comments-heading {
13 .comments-heading {
14 margin-bottom: -1px;
14 margin-bottom: -1px;
15 background: @grey6;
15 background: @grey6;
16 display: block;
16 display: block;
17 padding: 10px 0px;
17 padding: 10px 0px;
18 font-size: 18px
18 font-size: 18px
19 }
19 }
20
20
21 #comment-tr-show {
21 #comment-tr-show {
22 padding: 5px 0;
22 padding: 5px 0;
23 }
23 }
24
24
25 tr.inline-comments div {
25 tr.inline-comments div {
26 max-width: 100%;
26 max-width: 100%;
27
27
28 p {
28 p {
29 white-space: normal;
29 white-space: normal;
30 }
30 }
31
31
32 code, pre, .code, dd {
32 code, pre, .code, dd {
33 overflow-x: auto;
33 overflow-x: auto;
34 width: 1062px;
34 width: 1062px;
35 }
35 }
36
36
37 dd {
37 dd {
38 width: auto;
38 width: auto;
39 }
39 }
40 }
40 }
41
41
42 #injected_page_comments {
42 #injected_page_comments {
43 .comment-previous-link,
43 .comment-previous-link,
44 .comment-next-link,
44 .comment-next-link,
45 .comment-links-divider {
45 .comment-links-divider {
46 display: none;
46 display: none;
47 }
47 }
48 }
48 }
49
49
50 .add-comment {
50 .add-comment {
51 margin-bottom: 10px;
51 margin-bottom: 10px;
52 }
52 }
53 .hide-comment-button .add-comment {
53 .hide-comment-button .add-comment {
54 display: none;
54 display: none;
55 }
55 }
56
56
57 .comment-bubble {
57 .comment-bubble {
58 color: @grey4;
58 color: @grey4;
59 margin-top: 4px;
59 margin-top: 4px;
60 margin-right: 30px;
60 margin-right: 30px;
61 visibility: hidden;
61 visibility: hidden;
62 }
62 }
63
63
64 .comment-draft {
64 .comment-draft {
65 float: left;
65 float: left;
66 margin-right: 10px;
66 margin-right: 10px;
67 font-weight: 600;
67 font-weight: 400;
68 color: @alert3;
68 color: @color-draft;
69 }
70
71 .comment-new {
72 float: left;
73 margin-right: 10px;
74 font-weight: 400;
75 color: @color-new;
69 }
76 }
70
77
71 .comment-label {
78 .comment-label {
72 float: left;
79 float: left;
73
80
74 padding: 0.4em 0.4em;
81 padding: 0 8px 0 0;
75 margin: 2px 4px 0px 0px;
76 display: inline-block;
77 min-height: 0;
82 min-height: 0;
78
83
79 text-align: center;
84 text-align: center;
80 font-size: 10px;
85 font-size: 10px;
81 line-height: .8em;
82
86
83 font-family: @text-italic;
87 font-family: @text-italic;
84 font-style: italic;
88 font-style: italic;
85 background: #fff none;
89 background: #fff none;
86 color: @grey3;
90 color: @grey3;
87 border: 1px solid @grey4;
88 white-space: nowrap;
91 white-space: nowrap;
89
92
90 text-transform: uppercase;
93 text-transform: uppercase;
91 min-width: 50px;
94 min-width: 50px;
92 border-radius: 4px;
93
95
94 &.todo {
96 &.todo {
95 color: @color5;
97 color: @color5;
96 font-style: italic;
98 font-style: italic;
97 font-weight: @text-bold-italic-weight;
99 font-weight: @text-bold-italic-weight;
98 font-family: @text-bold-italic;
100 font-family: @text-bold-italic;
99 }
101 }
100
102
101 .resolve {
103 .resolve {
102 cursor: pointer;
104 cursor: pointer;
103 text-decoration: underline;
105 text-decoration: underline;
104 }
106 }
105
107
106 .resolved {
108 .resolved {
107 text-decoration: line-through;
109 text-decoration: line-through;
108 color: @color1;
110 color: @color1;
109 }
111 }
110 .resolved a {
112 .resolved a {
111 text-decoration: line-through;
113 text-decoration: line-through;
112 color: @color1;
114 color: @color1;
113 }
115 }
114 .resolve-text {
116 .resolve-text {
115 color: @color1;
117 color: @color1;
116 margin: 2px 8px;
118 margin: 2px 8px;
117 font-family: @text-italic;
119 font-family: @text-italic;
118 font-style: italic;
120 font-style: italic;
119 }
121 }
120 }
122 }
121
123
122 .has-spacer-after {
124 .has-spacer-after {
123 &:after {
125 &:after {
124 content: ' | ';
126 content: ' | ';
125 color: @grey5;
127 color: @grey5;
126 }
128 }
127 }
129 }
128
130
129 .has-spacer-before {
131 .has-spacer-before {
130 &:before {
132 &:before {
131 content: ' | ';
133 content: ' | ';
132 color: @grey5;
134 color: @grey5;
133 }
135 }
134 }
136 }
135
137
136 .comment {
138 .comment {
137
139
138 &.comment-general {
140 &.comment-general {
139 border: 1px solid @grey5;
141 border: 1px solid @grey5;
140 padding: 5px 5px 5px 5px;
142 padding: 5px 5px 5px 5px;
141 }
143 }
142
144
143 margin: @padding 0;
145 margin: @padding 0;
144 padding: 4px 0 0 0;
146 padding: 4px 0 0 0;
145 line-height: 1em;
147 line-height: 1em;
146
148
147 .rc-user {
149 .rc-user {
148 min-width: 0;
150 min-width: 0;
149 margin: 0px .5em 0 0;
151 margin: 0px .5em 0 0;
150
152
151 .user {
153 .user {
152 display: inline;
154 display: inline;
153 }
155 }
154 }
156 }
155
157
156 .meta {
158 .meta {
157 position: relative;
159 position: relative;
158 width: 100%;
160 width: 100%;
159 border-bottom: 1px solid @grey5;
161 border-bottom: 1px solid @grey5;
160 margin: -5px 0px;
162 margin: -5px 0px;
161 line-height: 24px;
163 line-height: 24px;
162
164
163 &:hover .permalink {
165 &:hover .permalink {
164 visibility: visible;
166 visibility: visible;
165 color: @rcblue;
167 color: @rcblue;
166 }
168 }
167 }
169 }
168
170
169 .author,
171 .author,
170 .date {
172 .date {
171 display: inline;
173 display: inline;
172
174
173 &:after {
175 &:after {
174 content: ' | ';
176 content: ' | ';
175 color: @grey5;
177 color: @grey5;
176 }
178 }
177 }
179 }
178
180
179 .author-general img {
181 .author-general img {
180 top: 3px;
182 top: 3px;
181 }
183 }
182 .author-inline img {
184 .author-inline img {
183 top: 3px;
185 top: 3px;
184 }
186 }
185
187
186 .status-change,
188 .status-change,
187 .permalink,
189 .permalink,
188 .changeset-status-lbl {
190 .changeset-status-lbl {
189 display: inline;
191 display: inline;
190 }
192 }
191
193
192 .permalink {
194 .permalink {
193 visibility: hidden;
195 visibility: hidden;
194 }
196 }
195
197
196 .comment-links-divider {
198 .comment-links-divider {
197 display: inline;
199 display: inline;
198 }
200 }
199
201
200 .comment-links-block {
202 .comment-links-block {
201 float:right;
203 float:right;
202 text-align: right;
204 text-align: right;
203 min-width: 85px;
205 min-width: 85px;
204
206
205 [class^="icon-"]:before,
207 [class^="icon-"]:before,
206 [class*=" icon-"]:before {
208 [class*=" icon-"]:before {
207 margin-left: 0;
209 margin-left: 0;
208 margin-right: 0;
210 margin-right: 0;
209 }
211 }
210 }
212 }
211
213
212 .comment-previous-link {
214 .comment-previous-link {
213 display: inline-block;
215 display: inline-block;
214
216
215 .arrow_comment_link{
217 .arrow_comment_link{
216 cursor: pointer;
218 cursor: pointer;
217 i {
219 i {
218 font-size:10px;
220 font-size:10px;
219 }
221 }
220 }
222 }
221 .arrow_comment_link.disabled {
223 .arrow_comment_link.disabled {
222 cursor: default;
224 cursor: default;
223 color: @grey5;
225 color: @grey5;
224 }
226 }
225 }
227 }
226
228
227 .comment-next-link {
229 .comment-next-link {
228 display: inline-block;
230 display: inline-block;
229
231
230 .arrow_comment_link{
232 .arrow_comment_link{
231 cursor: pointer;
233 cursor: pointer;
232 i {
234 i {
233 font-size:10px;
235 font-size:10px;
234 }
236 }
235 }
237 }
236 .arrow_comment_link.disabled {
238 .arrow_comment_link.disabled {
237 cursor: default;
239 cursor: default;
238 color: @grey5;
240 color: @grey5;
239 }
241 }
240 }
242 }
241
243
242 .delete-comment {
244 .delete-comment {
243 display: inline-block;
245 display: inline-block;
244 color: @rcblue;
246 color: @rcblue;
245
247
246 &:hover {
248 &:hover {
247 cursor: pointer;
249 cursor: pointer;
248 }
250 }
249 }
251 }
250
252
251 .text {
253 .text {
252 clear: both;
254 clear: both;
253 .border-radius(@border-radius);
255 .border-radius(@border-radius);
254 .box-sizing(border-box);
256 .box-sizing(border-box);
255
257
256 .markdown-block p,
258 .markdown-block p,
257 .rst-block p {
259 .rst-block p {
258 margin: .5em 0 !important;
260 margin: .5em 0 !important;
259 // TODO: lisa: This is needed because of other rst !important rules :[
261 // TODO: lisa: This is needed because of other rst !important rules :[
260 }
262 }
261 }
263 }
262
264
263 .pr-version {
265 .pr-version {
264 display: inline-block;
266 display: inline-block;
265 }
267 }
266 .pr-version-inline {
268 .pr-version-inline {
267 display: inline-block;
269 display: inline-block;
268 }
270 }
269 .pr-version-num {
271 .pr-version-num {
270 font-size: 10px;
272 font-size: 10px;
271 }
273 }
272 }
274 }
273
275
274 @comment-padding: 5px;
276 @comment-padding: 5px;
275
277
276 .general-comments {
278 .general-comments {
277 .comment-outdated {
279 .comment-outdated {
278 opacity: @comment-outdated-opacity;
280 opacity: @comment-outdated-opacity;
279 }
281 }
282
283 .comment-outdated-label {
284 color: @grey3;
285 padding-right: 4px;
286 }
280 }
287 }
281
288
282 .inline-comments {
289 .inline-comments {
283 border-radius: @border-radius;
290
284 .comment {
291 .comment {
285 margin: 0;
292 margin: 0;
286 border-radius: @border-radius;
287 }
293 }
294
288 .comment-outdated {
295 .comment-outdated {
289 opacity: @comment-outdated-opacity;
296 opacity: @comment-outdated-opacity;
290 }
297 }
291
298
299 .comment-outdated-label {
300 color: @grey3;
301 padding-right: 4px;
302 }
303
292 .comment-inline {
304 .comment-inline {
305
306 &:first-child {
307 margin: 4px 4px 0 4px;
308 border-top: 1px solid @grey5;
309 border-bottom: 0 solid @grey5;
310 border-left: 1px solid @grey5;
311 border-right: 1px solid @grey5;
312 .border-radius-top(4px);
313 }
314
315 &:only-child {
316 margin: 4px 4px 0 4px;
317 border-top: 1px solid @grey5;
318 border-bottom: 0 solid @grey5;
319 border-left: 1px solid @grey5;
320 border-right: 1px solid @grey5;
321 .border-radius-top(4px);
322 }
323
293 background: white;
324 background: white;
294 padding: @comment-padding @comment-padding;
325 padding: @comment-padding @comment-padding;
295 border: @comment-padding solid @grey6;
326 margin: 0 4px 0 4px;
327 border-top: 0 solid @grey5;
328 border-bottom: 0 solid @grey5;
329 border-left: 1px solid @grey5;
330 border-right: 1px solid @grey5;
296
331
297 .text {
332 .text {
298 border: none;
333 border: none;
299 }
334 }
335
300 .meta {
336 .meta {
301 border-bottom: 1px solid @grey6;
337 border-bottom: 1px solid @grey6;
302 margin: -5px 0px;
338 margin: -5px 0px;
303 line-height: 24px;
339 line-height: 24px;
304 }
340 }
341
305 }
342 }
306 .comment-selected {
343 .comment-selected {
307 border-left: 6px solid @comment-highlight-color;
344 border-left: 6px solid @comment-highlight-color;
308 }
345 }
346
347 .comment-inline-form-open {
348 display: block !important;
349 }
350
309 .comment-inline-form {
351 .comment-inline-form {
310 padding: @comment-padding;
311 display: none;
352 display: none;
312 }
353 }
313 .cb-comment-add-button {
354
314 margin: @comment-padding;
355 .comment-inline-form-edit {
356 padding: 0;
357 margin: 0px 4px 2px 4px;
358 }
359
360 .reply-thread-container {
361 display: table;
362 width: 100%;
363 padding: 0px 4px 4px 4px;
364 }
365
366 .reply-thread-container-wrapper {
367 margin: 0 4px 4px 4px;
368 border-top: 0 solid @grey5;
369 border-bottom: 1px solid @grey5;
370 border-left: 1px solid @grey5;
371 border-right: 1px solid @grey5;
372 .border-radius-bottom(4px);
373 }
374
375 .reply-thread-gravatar {
376 display: table-cell;
377 width: 24px;
378 height: 24px;
379 padding-top: 10px;
380 padding-left: 10px;
381 background-color: #eeeeee;
382 vertical-align: top;
315 }
383 }
384
385 .reply-thread-reply-button {
386 display: table-cell;
387 width: 100%;
388 height: 33px;
389 padding: 3px 8px;
390 margin-left: 8px;
391 background-color: #eeeeee;
392 }
393
394 .reply-thread-reply-button .cb-comment-add-button {
395 border-radius: 4px;
396 width: 100%;
397 padding: 6px 2px;
398 text-align: left;
399 cursor: text;
400 color: @grey3;
401 }
402 .reply-thread-reply-button .cb-comment-add-button:hover {
403 background-color: white;
404 color: @grey2;
405 }
406
407 .reply-thread-last {
408 display: table-cell;
409 width: 10px;
410 }
411
412 /* Hide reply box when it's a first element,
413 can happen when drafts are saved but not shown to specific user,
414 or there are outdated comments hidden
415 */
416 .reply-thread-container-wrapper:first-child:not(.comment-form-active) {
417 display: none;
418 }
419
420 .reply-thread-container-wrapper.comment-outdated {
421 display: none
422 }
423
316 /* hide add comment button when form is open */
424 /* hide add comment button when form is open */
317 .comment-inline-form-open ~ .cb-comment-add-button {
425 .comment-inline-form-open ~ .cb-comment-add-button {
318 display: none;
426 display: none;
319 }
427 }
320 .comment-inline-form-open {
428
321 display: block;
429 /* hide add comment button when only comment is being deleted */
430 .comment-deleting:first-child + .cb-comment-add-button {
431 display: none;
322 }
432 }
433
323 /* hide add comment button when form but no comments */
434 /* hide add comment button when form but no comments */
324 .comment-inline-form:first-child + .cb-comment-add-button {
435 .comment-inline-form:first-child + .cb-comment-add-button {
325 display: none;
436 display: none;
326 }
437 }
327 /* hide add comment button when no comments or form */
438
328 .cb-comment-add-button:first-child {
329 display: none;
330 }
439 }
331 /* hide add comment button when only comment is being deleted */
332 .comment-deleting:first-child + .cb-comment-add-button {
333 display: none;
334 }
335 }
336
337
440
338 .show-outdated-comments {
441 .show-outdated-comments {
339 display: inline;
442 display: inline;
340 color: @rcblue;
443 color: @rcblue;
341 }
444 }
342
445
343 // Comment Form
446 // Comment Form
344 div.comment-form {
447 div.comment-form {
345 margin-top: 20px;
448 margin-top: 20px;
346 }
449 }
347
450
348 .comment-form strong {
451 .comment-form strong {
349 display: block;
452 display: block;
350 margin-bottom: 15px;
453 margin-bottom: 15px;
351 }
454 }
352
455
353 .comment-form textarea {
456 .comment-form textarea {
354 width: 100%;
457 width: 100%;
355 height: 100px;
458 height: 100px;
356 font-family: @text-monospace;
459 font-family: @text-monospace;
357 }
460 }
358
461
359 form.comment-form {
462 form.comment-form {
360 margin-top: 10px;
463 margin-top: 10px;
361 margin-left: 10px;
464 margin-left: 10px;
362 }
465 }
363
466
364 .comment-inline-form .comment-block-ta,
467 .comment-inline-form .comment-block-ta,
365 .comment-form .comment-block-ta,
468 .comment-form .comment-block-ta,
366 .comment-form .preview-box {
469 .comment-form .preview-box {
367 .border-radius(@border-radius);
470 .border-radius(@border-radius);
368 .box-sizing(border-box);
471 .box-sizing(border-box);
369 background-color: white;
472 background-color: white;
370 }
473 }
371
474
372 .comment-form-submit {
475 .comment-form-submit {
373 margin-top: 5px;
476 margin-top: 5px;
374 margin-left: 525px;
477 margin-left: 525px;
375 }
478 }
376
479
377 .file-comments {
480 .file-comments {
378 display: none;
481 display: none;
379 }
482 }
380
483
381 .comment-form .preview-box.unloaded,
484 .comment-form .preview-box.unloaded,
382 .comment-inline-form .preview-box.unloaded {
485 .comment-inline-form .preview-box.unloaded {
383 height: 50px;
486 height: 50px;
384 text-align: center;
487 text-align: center;
385 padding: 20px;
488 padding: 20px;
386 background-color: white;
489 background-color: white;
387 }
490 }
388
491
389 .comment-footer {
492 .comment-footer {
390 position: relative;
493 display: table;
391 width: 100%;
494 width: 100%;
392 min-height: 42px;
495 height: 42px;
393
496
394 .status_box,
497 .comment-status-box,
395 .cancel-button {
498 .cancel-button {
396 float: left;
397 display: inline-block;
499 display: inline-block;
398 }
500 }
399
501
400 .status_box {
502 .comment-status-box {
401 margin-left: 10px;
503 margin-left: 10px;
402 }
504 }
403
505
404 .action-buttons {
506 .action-buttons {
405 float: left;
507 display: table-cell;
406 display: inline-block;
508 padding: 5px 0 5px 2px;
509 }
510
511 .toolbar-text {
512 height: 42px;
513 display: table-cell;
514 vertical-align: bottom;
515 font-size: 11px;
516 color: @grey4;
517 text-align: right;
518
519 a {
520 color: @grey4;
521 }
522
523 p {
524 padding: 0;
525 margin: 0;
526 }
407 }
527 }
408
528
409 .action-buttons-extra {
529 .action-buttons-extra {
410 display: inline-block;
530 display: inline-block;
411 }
531 }
412 }
532 }
413
533
414 .comment-form {
534 .comment-form {
415
535
416 .comment {
536 .comment {
417 margin-left: 10px;
537 margin-left: 10px;
418 }
538 }
419
539
420 .comment-help {
540 .comment-help {
421 color: @grey4;
541 color: @grey4;
422 padding: 5px 0 5px 0;
542 padding: 5px 0 5px 0;
423 }
543 }
424
544
425 .comment-title {
545 .comment-title {
426 padding: 5px 0 5px 0;
546 padding: 5px 0 5px 0;
427 }
547 }
428
548
429 .comment-button {
549 .comment-button {
430 display: inline-block;
550 display: inline-block;
431 }
551 }
432
552
433 .comment-button-input {
553 .comment-button-input {
434 margin-right: 0;
554 margin-right: 0;
435 }
555 }
436
556
437 .comment-footer {
438 margin-bottom: 50px;
439 margin-top: 10px;
440 }
441 }
557 }
442
558
443
559
444 .comment-form-login {
560 .comment-form-login {
445 .comment-help {
561 .comment-help {
446 padding: 0.7em; //same as the button
562 padding: 0.7em; //same as the button
447 }
563 }
448
564
449 div.clearfix {
565 div.clearfix {
450 clear: both;
566 clear: both;
451 width: 100%;
567 width: 100%;
452 display: block;
568 display: block;
453 }
569 }
454 }
570 }
455
571
456 .comment-version-select {
572 .comment-version-select {
457 margin: 0px;
573 margin: 0px;
458 border-radius: inherit;
574 border-radius: inherit;
459 border-color: @grey6;
575 border-color: @grey6;
460 height: 20px;
576 height: 20px;
461 }
577 }
462
578
463 .comment-type {
579 .comment-type {
464 margin: 0px;
580 margin: 0px;
465 border-radius: inherit;
581 border-radius: inherit;
466 border-color: @grey6;
582 border-color: @grey6;
467 }
583 }
468
584
469 .preview-box {
585 .preview-box {
470 min-height: 105px;
586 min-height: 105px;
471 margin-bottom: 15px;
587 margin-bottom: 15px;
472 background-color: white;
588 background-color: white;
473 .border-radius(@border-radius);
589 .border-radius(@border-radius);
474 .box-sizing(border-box);
590 .box-sizing(border-box);
475 }
591 }
476
592
477 .add-another-button {
593 .add-another-button {
478 margin-left: 10px;
594 margin-left: 10px;
479 margin-top: 10px;
595 margin-top: 10px;
480 margin-bottom: 10px;
596 margin-bottom: 10px;
481 }
597 }
482
598
483 .comment .buttons {
599 .comment .buttons {
484 float: right;
600 float: right;
485 margin: -1px 0px 0px 0px;
601 margin: -1px 0px 0px 0px;
486 }
602 }
487
603
488 // Inline Comment Form
604 // Inline Comment Form
489 .injected_diff .comment-inline-form,
605 .injected_diff .comment-inline-form,
490 .comment-inline-form {
606 .comment-inline-form {
491 background-color: white;
607 background-color: white;
492 margin-top: 10px;
608 margin-top: 4px;
493 margin-bottom: 20px;
609 margin-bottom: 10px;
494 }
610 }
495
611
496 .inline-form {
612 .inline-form {
497 padding: 10px 7px;
613 padding: 10px 7px;
498 }
614 }
499
615
500 .inline-form div {
616 .inline-form div {
501 max-width: 100%;
617 max-width: 100%;
502 }
618 }
503
619
504 .overlay {
620 .overlay {
505 display: none;
621 display: none;
506 position: absolute;
622 position: absolute;
507 width: 100%;
623 width: 100%;
508 text-align: center;
624 text-align: center;
509 vertical-align: middle;
625 vertical-align: middle;
510 font-size: 16px;
626 font-size: 16px;
511 background: none repeat scroll 0 0 white;
627 background: none repeat scroll 0 0 white;
512
628
513 &.submitting {
629 &.submitting {
514 display: block;
630 display: block;
515 opacity: 0.5;
631 opacity: 0.5;
516 z-index: 100;
632 z-index: 100;
517 }
633 }
518 }
634 }
519 .comment-inline-form .overlay.submitting .overlay-text {
635 .comment-inline-form .overlay.submitting .overlay-text {
520 margin-top: 5%;
636 margin-top: 5%;
521 }
637 }
522
638
523 .comment-inline-form .clearfix,
639 .comment-inline-form .clearfix,
524 .comment-form .clearfix {
640 .comment-form .clearfix {
525 .border-radius(@border-radius);
641 .border-radius(@border-radius);
526 margin: 0px;
642 margin: 0px;
527 }
643 }
528
644
529 .comment-inline-form .comment-footer {
530 margin: 10px 0px 0px 0px;
531 }
532
645
533 .hide-inline-form-button {
646 .hide-inline-form-button {
534 margin-left: 5px;
647 margin-left: 5px;
535 }
648 }
536 .comment-button .hide-inline-form {
649 .comment-button .hide-inline-form {
537 background: white;
650 background: white;
538 }
651 }
539
652
540 .comment-area {
653 .comment-area {
541 padding: 6px 8px;
654 padding: 6px 8px;
542 border: 1px solid @grey5;
655 border: 1px solid @grey5;
543 .border-radius(@border-radius);
656 .border-radius(@border-radius);
544
657
545 .resolve-action {
658 .resolve-action {
546 padding: 1px 0px 0px 6px;
659 padding: 1px 0px 0px 6px;
547 }
660 }
548
661
549 }
662 }
550
663
551 comment-area-text {
664 comment-area-text {
552 color: @grey3;
665 color: @grey3;
553 }
666 }
554
667
555 .comment-area-header {
668 .comment-area-header {
556 height: 35px;
669 height: 35px;
670 border-bottom: 1px solid @grey5;
557 }
671 }
558
672
559 .comment-area-header .nav-links {
673 .comment-area-header .nav-links {
560 display: flex;
674 display: flex;
561 flex-flow: row wrap;
675 flex-flow: row wrap;
562 -webkit-flex-flow: row wrap;
676 -webkit-flex-flow: row wrap;
563 width: 100%;
677 width: 100%;
678 border: none;
564 }
679 }
565
680
566 .comment-area-footer {
681 .comment-area-footer {
567 min-height: 30px;
682 min-height: 30px;
568 }
683 }
569
684
570 .comment-footer .toolbar {
685 .comment-footer .toolbar {
571
686
572 }
687 }
573
688
574 .comment-attachment-uploader {
689 .comment-attachment-uploader {
575 border: 1px dashed white;
690 border: 1px dashed white;
576 border-radius: @border-radius;
691 border-radius: @border-radius;
577 margin-top: -10px;
692 margin-top: -10px;
578 line-height: 30px;
693 line-height: 30px;
579 &.dz-drag-hover {
694 &.dz-drag-hover {
580 border-color: @grey3;
695 border-color: @grey3;
581 }
696 }
582
697
583 .dz-error-message {
698 .dz-error-message {
584 padding-top: 0;
699 padding-top: 0;
585 }
700 }
586 }
701 }
587
702
588 .comment-attachment-text {
703 .comment-attachment-text {
589 clear: both;
704 clear: both;
590 font-size: 11px;
705 font-size: 11px;
591 color: #8F8F8F;
706 color: #8F8F8F;
592 width: 100%;
707 width: 100%;
593 .pick-attachment {
708 .pick-attachment {
594 color: #8F8F8F;
709 color: #8F8F8F;
595 }
710 }
596 .pick-attachment:hover {
711 .pick-attachment:hover {
597 color: @rcblue;
712 color: @rcblue;
598 }
713 }
599 }
714 }
600
715
601 .nav-links {
716 .nav-links {
602 padding: 0;
717 padding: 0;
603 margin: 0;
718 margin: 0;
604 list-style: none;
719 list-style: none;
605 height: auto;
720 height: auto;
606 border-bottom: 1px solid @grey5;
721 border-bottom: 1px solid @grey5;
607 }
722 }
608 .nav-links li {
723 .nav-links li {
609 display: inline-block;
724 display: inline-block;
610 list-style-type: none;
725 list-style-type: none;
611 }
726 }
612
727
613 .nav-links li a.disabled {
728 .nav-links li a.disabled {
614 cursor: not-allowed;
729 cursor: not-allowed;
615 }
730 }
616
731
617 .nav-links li.active a {
732 .nav-links li.active a {
618 border-bottom: 2px solid @rcblue;
733 border-bottom: 2px solid @rcblue;
619 color: #000;
734 color: #000;
620 font-weight: 600;
735 font-weight: 600;
621 }
736 }
622 .nav-links li a {
737 .nav-links li a {
623 display: inline-block;
738 display: inline-block;
624 padding: 0px 10px 5px 10px;
739 padding: 0px 10px 5px 10px;
625 margin-bottom: -1px;
740 margin-bottom: -1px;
626 font-size: 14px;
741 font-size: 14px;
627 line-height: 28px;
742 line-height: 28px;
628 color: #8f8f8f;
743 color: #8f8f8f;
629 border-bottom: 2px solid transparent;
744 border-bottom: 2px solid transparent;
630 }
745 }
631
746
632 .toolbar-text {
633 float: right;
634 font-size: 11px;
635 color: @grey4;
636 text-align: right;
637
638 a {
639 color: @grey4;
640 }
641 }
642
@@ -1,3239 +1,3243 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'variables';
8 @import 'variables';
9 @import 'bootstrap-variables';
9 @import 'bootstrap-variables';
10 @import 'form-bootstrap';
10 @import 'form-bootstrap';
11 @import 'codemirror';
11 @import 'codemirror';
12 @import 'legacy_code_styles';
12 @import 'legacy_code_styles';
13 @import 'readme-box';
13 @import 'readme-box';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29 @import 'tooltips';
29 @import 'tooltips';
30 @import 'sweetalert2';
30 @import 'sweetalert2';
31
31
32
32
33 //--- BASE ------------------//
33 //--- BASE ------------------//
34 .noscript-error {
34 .noscript-error {
35 top: 0;
35 top: 0;
36 left: 0;
36 left: 0;
37 width: 100%;
37 width: 100%;
38 z-index: 101;
38 z-index: 101;
39 text-align: center;
39 text-align: center;
40 font-size: 120%;
40 font-size: 120%;
41 color: white;
41 color: white;
42 background-color: @alert2;
42 background-color: @alert2;
43 padding: 5px 0 5px 0;
43 padding: 5px 0 5px 0;
44 font-weight: @text-semibold-weight;
44 font-weight: @text-semibold-weight;
45 font-family: @text-semibold;
45 font-family: @text-semibold;
46 }
46 }
47
47
48 html {
48 html {
49 display: table;
49 display: table;
50 height: 100%;
50 height: 100%;
51 width: 100%;
51 width: 100%;
52 }
52 }
53
53
54 body {
54 body {
55 display: table-cell;
55 display: table-cell;
56 width: 100%;
56 width: 100%;
57 }
57 }
58
58
59 //--- LAYOUT ------------------//
59 //--- LAYOUT ------------------//
60
60
61 .hidden{
61 .hidden{
62 display: none !important;
62 display: none !important;
63 }
63 }
64
64
65 .box{
65 .box{
66 float: left;
66 float: left;
67 width: 100%;
67 width: 100%;
68 }
68 }
69
69
70 .browser-header {
70 .browser-header {
71 clear: both;
71 clear: both;
72 }
72 }
73 .main {
73 .main {
74 clear: both;
74 clear: both;
75 padding:0 0 @pagepadding;
75 padding:0 0 @pagepadding;
76 height: auto;
76 height: auto;
77
77
78 &:after { //clearfix
78 &:after { //clearfix
79 content:"";
79 content:"";
80 clear:both;
80 clear:both;
81 width:100%;
81 width:100%;
82 display:block;
82 display:block;
83 }
83 }
84 }
84 }
85
85
86 .flex-container {
86 .flex-container {
87 display: flex;
87 display: flex;
88 justify-content: space-between;
88 justify-content: space-between;
89 }
89 }
90
90
91 .action-link{
91 .action-link{
92 margin-left: @padding;
92 margin-left: @padding;
93 padding-left: @padding;
93 padding-left: @padding;
94 border-left: @border-thickness solid @border-default-color;
94 border-left: @border-thickness solid @border-default-color;
95 }
95 }
96
96
97 .cursor-pointer {
97 .cursor-pointer {
98 cursor: pointer;
98 cursor: pointer;
99 }
99 }
100
100
101 input + .action-link, .action-link.first{
101 input + .action-link, .action-link.first{
102 border-left: none;
102 border-left: none;
103 }
103 }
104
104
105 .link-disabled {
105 .link-disabled {
106 color: @grey4;
106 color: @grey4;
107 cursor: default;
107 cursor: default;
108 }
108 }
109
109
110 .action-link.last{
110 .action-link.last{
111 margin-right: @padding;
111 margin-right: @padding;
112 padding-right: @padding;
112 padding-right: @padding;
113 }
113 }
114
114
115 .action-link.active,
115 .action-link.active,
116 .action-link.active a{
116 .action-link.active a{
117 color: @grey4;
117 color: @grey4;
118 }
118 }
119
119
120 .action-link.disabled {
120 .action-link.disabled {
121 color: @grey4;
121 color: @grey4;
122 cursor: inherit;
122 cursor: inherit;
123 }
123 }
124
124
125 .grey-link-action {
125 .grey-link-action {
126 cursor: pointer;
126 cursor: pointer;
127 &:hover {
127 &:hover {
128 color: @grey2;
128 color: @grey2;
129 }
129 }
130 color: @grey4;
130 color: @grey4;
131 }
131 }
132
132
133 .clipboard-action {
133 .clipboard-action {
134 cursor: pointer;
134 cursor: pointer;
135 margin-left: 5px;
135 margin-left: 5px;
136
136
137 &:not(.no-grey) {
137 &:not(.no-grey) {
138
138
139 &:hover {
139 &:hover {
140 color: @grey2;
140 color: @grey2;
141 }
141 }
142 color: @grey4;
142 color: @grey4;
143 }
143 }
144 }
144 }
145
145
146 ul.simple-list{
146 ul.simple-list{
147 list-style: none;
147 list-style: none;
148 margin: 0;
148 margin: 0;
149 padding: 0;
149 padding: 0;
150 }
150 }
151
151
152 .main-content {
152 .main-content {
153 padding-bottom: @pagepadding;
153 padding-bottom: @pagepadding;
154 }
154 }
155
155
156 .wide-mode-wrapper {
156 .wide-mode-wrapper {
157 max-width:4000px !important;
157 max-width:4000px !important;
158 }
158 }
159
159
160 .wrapper {
160 .wrapper {
161 position: relative;
161 position: relative;
162 max-width: @wrapper-maxwidth;
162 max-width: @wrapper-maxwidth;
163 margin: 0 auto;
163 margin: 0 auto;
164 }
164 }
165
165
166 #content {
166 #content {
167 clear: both;
167 clear: both;
168 padding: 0 @contentpadding;
168 padding: 0 @contentpadding;
169 }
169 }
170
170
171 .advanced-settings-fields{
171 .advanced-settings-fields{
172 input{
172 input{
173 margin-left: @textmargin;
173 margin-left: @textmargin;
174 margin-right: @padding/2;
174 margin-right: @padding/2;
175 }
175 }
176 }
176 }
177
177
178 .cs_files_title {
178 .cs_files_title {
179 margin: @pagepadding 0 0;
179 margin: @pagepadding 0 0;
180 }
180 }
181
181
182 input.inline[type="file"] {
182 input.inline[type="file"] {
183 display: inline;
183 display: inline;
184 }
184 }
185
185
186 .error_page {
186 .error_page {
187 margin: 10% auto;
187 margin: 10% auto;
188
188
189 h1 {
189 h1 {
190 color: @grey2;
190 color: @grey2;
191 }
191 }
192
192
193 .alert {
193 .alert {
194 margin: @padding 0;
194 margin: @padding 0;
195 }
195 }
196
196
197 .error-branding {
197 .error-branding {
198 color: @grey4;
198 color: @grey4;
199 font-weight: @text-semibold-weight;
199 font-weight: @text-semibold-weight;
200 font-family: @text-semibold;
200 font-family: @text-semibold;
201 }
201 }
202
202
203 .error_message {
203 .error_message {
204 font-family: @text-regular;
204 font-family: @text-regular;
205 }
205 }
206
206
207 .sidebar {
207 .sidebar {
208 min-height: 275px;
208 min-height: 275px;
209 margin: 0;
209 margin: 0;
210 padding: 0 0 @sidebarpadding @sidebarpadding;
210 padding: 0 0 @sidebarpadding @sidebarpadding;
211 border: none;
211 border: none;
212 }
212 }
213
213
214 .main-content {
214 .main-content {
215 position: relative;
215 position: relative;
216 margin: 0 @sidebarpadding @sidebarpadding;
216 margin: 0 @sidebarpadding @sidebarpadding;
217 padding: 0 0 0 @sidebarpadding;
217 padding: 0 0 0 @sidebarpadding;
218 border-left: @border-thickness solid @grey5;
218 border-left: @border-thickness solid @grey5;
219
219
220 @media (max-width:767px) {
220 @media (max-width:767px) {
221 clear: both;
221 clear: both;
222 width: 100%;
222 width: 100%;
223 margin: 0;
223 margin: 0;
224 border: none;
224 border: none;
225 }
225 }
226 }
226 }
227
227
228 .inner-column {
228 .inner-column {
229 float: left;
229 float: left;
230 width: 29.75%;
230 width: 29.75%;
231 min-height: 150px;
231 min-height: 150px;
232 margin: @sidebarpadding 2% 0 0;
232 margin: @sidebarpadding 2% 0 0;
233 padding: 0 2% 0 0;
233 padding: 0 2% 0 0;
234 border-right: @border-thickness solid @grey5;
234 border-right: @border-thickness solid @grey5;
235
235
236 @media (max-width:767px) {
236 @media (max-width:767px) {
237 clear: both;
237 clear: both;
238 width: 100%;
238 width: 100%;
239 border: none;
239 border: none;
240 }
240 }
241
241
242 ul {
242 ul {
243 padding-left: 1.25em;
243 padding-left: 1.25em;
244 }
244 }
245
245
246 &:last-child {
246 &:last-child {
247 margin: @sidebarpadding 0 0;
247 margin: @sidebarpadding 0 0;
248 border: none;
248 border: none;
249 }
249 }
250
250
251 h4 {
251 h4 {
252 margin: 0 0 @padding;
252 margin: 0 0 @padding;
253 font-weight: @text-semibold-weight;
253 font-weight: @text-semibold-weight;
254 font-family: @text-semibold;
254 font-family: @text-semibold;
255 }
255 }
256 }
256 }
257 }
257 }
258 .error-page-logo {
258 .error-page-logo {
259 width: 130px;
259 width: 130px;
260 height: 160px;
260 height: 160px;
261 }
261 }
262
262
263 // HEADER
263 // HEADER
264 .header {
264 .header {
265
265
266 // TODO: johbo: Fix login pages, so that they work without a min-height
266 // TODO: johbo: Fix login pages, so that they work without a min-height
267 // for the header and then remove the min-height. I chose a smaller value
267 // for the header and then remove the min-height. I chose a smaller value
268 // intentionally here to avoid rendering issues in the main navigation.
268 // intentionally here to avoid rendering issues in the main navigation.
269 min-height: 49px;
269 min-height: 49px;
270 min-width: 1024px;
270 min-width: 1024px;
271
271
272 position: relative;
272 position: relative;
273 vertical-align: bottom;
273 vertical-align: bottom;
274 padding: 0 @header-padding;
274 padding: 0 @header-padding;
275 background-color: @grey1;
275 background-color: @grey1;
276 color: @grey5;
276 color: @grey5;
277
277
278 .title {
278 .title {
279 overflow: visible;
279 overflow: visible;
280 }
280 }
281
281
282 &:before,
282 &:before,
283 &:after {
283 &:after {
284 content: "";
284 content: "";
285 clear: both;
285 clear: both;
286 width: 100%;
286 width: 100%;
287 }
287 }
288
288
289 // TODO: johbo: Avoids breaking "Repositories" chooser
289 // TODO: johbo: Avoids breaking "Repositories" chooser
290 .select2-container .select2-choice .select2-arrow {
290 .select2-container .select2-choice .select2-arrow {
291 display: none;
291 display: none;
292 }
292 }
293 }
293 }
294
294
295 #header-inner {
295 #header-inner {
296 &.title {
296 &.title {
297 margin: 0;
297 margin: 0;
298 }
298 }
299 &:before,
299 &:before,
300 &:after {
300 &:after {
301 content: "";
301 content: "";
302 clear: both;
302 clear: both;
303 }
303 }
304 }
304 }
305
305
306 // Gists
306 // Gists
307 #files_data {
307 #files_data {
308 clear: both; //for firefox
308 clear: both; //for firefox
309 padding-top: 10px;
309 padding-top: 10px;
310 }
310 }
311
311
312 #gistid {
312 #gistid {
313 margin-right: @padding;
313 margin-right: @padding;
314 }
314 }
315
315
316 // Global Settings Editor
316 // Global Settings Editor
317 .textarea.editor {
317 .textarea.editor {
318 float: left;
318 float: left;
319 position: relative;
319 position: relative;
320 max-width: @texteditor-width;
320 max-width: @texteditor-width;
321
321
322 select {
322 select {
323 position: absolute;
323 position: absolute;
324 top:10px;
324 top:10px;
325 right:0;
325 right:0;
326 }
326 }
327
327
328 .CodeMirror {
328 .CodeMirror {
329 margin: 0;
329 margin: 0;
330 }
330 }
331
331
332 .help-block {
332 .help-block {
333 margin: 0 0 @padding;
333 margin: 0 0 @padding;
334 padding:.5em;
334 padding:.5em;
335 background-color: @grey6;
335 background-color: @grey6;
336 &.pre-formatting {
336 &.pre-formatting {
337 white-space: pre;
337 white-space: pre;
338 }
338 }
339 }
339 }
340 }
340 }
341
341
342 ul.auth_plugins {
342 ul.auth_plugins {
343 margin: @padding 0 @padding @legend-width;
343 margin: @padding 0 @padding @legend-width;
344 padding: 0;
344 padding: 0;
345
345
346 li {
346 li {
347 margin-bottom: @padding;
347 margin-bottom: @padding;
348 line-height: 1em;
348 line-height: 1em;
349 list-style-type: none;
349 list-style-type: none;
350
350
351 .auth_buttons .btn {
351 .auth_buttons .btn {
352 margin-right: @padding;
352 margin-right: @padding;
353 }
353 }
354
354
355 }
355 }
356 }
356 }
357
357
358
358
359 // My Account PR list
359 // My Account PR list
360
360
361 #show_closed {
361 #show_closed {
362 margin: 0 1em 0 0;
362 margin: 0 1em 0 0;
363 }
363 }
364
364
365 #pull_request_list_table {
365 #pull_request_list_table {
366 .closed {
366 .closed {
367 background-color: @grey6;
367 background-color: @grey6;
368 }
368 }
369
369
370 .state-creating,
370 .state-creating,
371 .state-updating,
371 .state-updating,
372 .state-merging
372 .state-merging
373 {
373 {
374 background-color: @grey6;
374 background-color: @grey6;
375 }
375 }
376
376
377 .log-container .truncate {
377 .log-container .truncate {
378 height: 2.75em;
378 height: 2.75em;
379 white-space: pre-line;
379 white-space: pre-line;
380 }
380 }
381 table.rctable .user {
381 table.rctable .user {
382 padding-left: 0;
382 padding-left: 0;
383 }
383 }
384 .td-status {
384 .td-status {
385 padding: 0 0px 0px 10px;
385 padding: 0 0px 0px 10px;
386 width: 15px;
386 width: 15px;
387 }
387 }
388 table.rctable {
388 table.rctable {
389 td.td-description,
389 td.td-description,
390 .rc-user {
390 .rc-user {
391 min-width: auto;
391 min-width: auto;
392 }
392 }
393 }
393 }
394 }
394 }
395
395
396 // Pull Requests
396 // Pull Requests
397
397
398 .pullrequests_section_head {
398 .pullrequests_section_head {
399 display: block;
399 display: block;
400 clear: both;
400 clear: both;
401 margin: @padding 0;
401 margin: @padding 0;
402 font-weight: @text-bold-weight;
402 font-weight: @text-bold-weight;
403 font-family: @text-bold;
403 font-family: @text-bold;
404 }
404 }
405
405
406 .pr-commit-flow {
406 .pr-commit-flow {
407 position: relative;
407 position: relative;
408 font-weight: 600;
408 font-weight: 600;
409
409
410 .tag {
410 .tag {
411 display: inline-block;
411 display: inline-block;
412 margin: 0 1em .5em 0;
412 margin: 0 1em .5em 0;
413 }
413 }
414
414
415 .clone-url {
415 .clone-url {
416 display: inline-block;
416 display: inline-block;
417 margin: 0 0 .5em 0;
417 margin: 0 0 .5em 0;
418 padding: 0;
418 padding: 0;
419 line-height: 1.2em;
419 line-height: 1.2em;
420 }
420 }
421 }
421 }
422
422
423 .pr-mergeinfo {
423 .pr-mergeinfo {
424 min-width: 95% !important;
424 min-width: 95% !important;
425 padding: 0 !important;
425 padding: 0 !important;
426 border: 0;
426 border: 0;
427 }
427 }
428 .pr-mergeinfo-copy {
428 .pr-mergeinfo-copy {
429 padding: 0 0;
429 padding: 0 0;
430 }
430 }
431
431
432 .pr-pullinfo {
432 .pr-pullinfo {
433 min-width: 95% !important;
433 min-width: 95% !important;
434 padding: 0 !important;
434 padding: 0 !important;
435 border: 0;
435 border: 0;
436 }
436 }
437 .pr-pullinfo-copy {
437 .pr-pullinfo-copy {
438 padding: 0 0;
438 padding: 0 0;
439 }
439 }
440
440
441 .pr-title-input {
441 .pr-title-input {
442 width: 100%;
442 width: 100%;
443 font-size: 18px;
443 font-size: 18px;
444 margin: 0 0 4px 0;
444 margin: 0 0 4px 0;
445 padding: 0;
445 padding: 0;
446 line-height: 1.7em;
446 line-height: 1.7em;
447 color: @text-color;
447 color: @text-color;
448 letter-spacing: .02em;
448 letter-spacing: .02em;
449 font-weight: @text-bold-weight;
449 font-weight: @text-bold-weight;
450 font-family: @text-bold;
450 font-family: @text-bold;
451
451
452 &:hover {
452 &:hover {
453 box-shadow: none;
453 box-shadow: none;
454 }
454 }
455 }
455 }
456
456
457 #pr-title {
457 #pr-title {
458 input {
458 input {
459 border: 1px transparent;
459 border: 1px transparent;
460 color: black;
460 color: black;
461 opacity: 1;
461 opacity: 1;
462 background: #fff;
462 background: #fff;
463 font-size: 18px;
463 font-size: 18px;
464 }
464 }
465 }
465 }
466
466
467 .pr-title-closed-tag {
467 .pr-title-closed-tag {
468 font-size: 16px;
468 font-size: 16px;
469 }
469 }
470
470
471 #pr-desc {
471 #pr-desc {
472 padding: 10px 0;
472 padding: 10px 0;
473
473
474 .markdown-block {
474 .markdown-block {
475 padding: 0;
475 padding: 0;
476 margin-bottom: -30px;
476 margin-bottom: -30px;
477 }
477 }
478 }
478 }
479
479
480 #pullrequest_title {
480 #pullrequest_title {
481 width: 100%;
481 width: 100%;
482 box-sizing: border-box;
482 box-sizing: border-box;
483 }
483 }
484
484
485 #pr_open_message {
485 #pr_open_message {
486 border: @border-thickness solid #fff;
486 border: @border-thickness solid #fff;
487 border-radius: @border-radius;
487 border-radius: @border-radius;
488 text-align: left;
488 text-align: left;
489 overflow: hidden;
489 overflow: hidden;
490 white-space: pre-line;
490 white-space: pre-line;
491 padding-top: 5px
491 padding-top: 5px
492 }
492 }
493
493
494 #add_reviewer {
494 #add_reviewer {
495 padding-top: 10px;
495 padding-top: 10px;
496 }
496 }
497
497
498 #add_reviewer_input,
498 #add_reviewer_input,
499 #add_observer_input {
499 #add_observer_input {
500 padding-top: 10px
500 padding-top: 10px
501 }
501 }
502
502
503 .pr-details-title-author-pref {
503 .pr-details-title-author-pref {
504 padding-right: 10px
504 padding-right: 10px
505 }
505 }
506
506
507 .label-pr-detail {
507 .label-pr-detail {
508 display: table-cell;
508 display: table-cell;
509 width: 120px;
509 width: 120px;
510 padding-top: 7.5px;
510 padding-top: 7.5px;
511 padding-bottom: 7.5px;
511 padding-bottom: 7.5px;
512 padding-right: 7.5px;
512 padding-right: 7.5px;
513 }
513 }
514
514
515 .source-details ul {
515 .source-details ul {
516 padding: 10px 16px;
516 padding: 10px 16px;
517 }
517 }
518
518
519 .source-details-action {
519 .source-details-action {
520 color: @grey4;
520 color: @grey4;
521 font-size: 11px
521 font-size: 11px
522 }
522 }
523
523
524 .pr-submit-button {
524 .pr-submit-button {
525 float: right;
525 float: right;
526 margin: 0 0 0 5px;
526 margin: 0 0 0 5px;
527 }
527 }
528
528
529 .pr-spacing-container {
529 .pr-spacing-container {
530 padding: 20px;
530 padding: 20px;
531 clear: both
531 clear: both
532 }
532 }
533
533
534 #pr-description-input {
534 #pr-description-input {
535 margin-bottom: 0;
535 margin-bottom: 0;
536 }
536 }
537
537
538 .pr-description-label {
538 .pr-description-label {
539 vertical-align: top;
539 vertical-align: top;
540 }
540 }
541
541
542 #open_edit_pullrequest {
542 #open_edit_pullrequest {
543 padding: 0;
543 padding: 0;
544 }
544 }
545
545
546 #close_edit_pullrequest {
546 #close_edit_pullrequest {
547
547
548 }
548 }
549
549
550 #delete_pullrequest {
550 #delete_pullrequest {
551 clear: inherit;
551 clear: inherit;
552
552
553 form {
553 form {
554 display: inline;
554 display: inline;
555 }
555 }
556
556
557 }
557 }
558
558
559 .perms_section_head {
559 .perms_section_head {
560 min-width: 625px;
560 min-width: 625px;
561
561
562 h2 {
562 h2 {
563 margin-bottom: 0;
563 margin-bottom: 0;
564 }
564 }
565
565
566 .label-checkbox {
566 .label-checkbox {
567 float: left;
567 float: left;
568 }
568 }
569
569
570 &.field {
570 &.field {
571 margin: @space 0 @padding;
571 margin: @space 0 @padding;
572 }
572 }
573
573
574 &:first-child.field {
574 &:first-child.field {
575 margin-top: 0;
575 margin-top: 0;
576
576
577 .label {
577 .label {
578 margin-top: 0;
578 margin-top: 0;
579 padding-top: 0;
579 padding-top: 0;
580 }
580 }
581
581
582 .radios {
582 .radios {
583 padding-top: 0;
583 padding-top: 0;
584 }
584 }
585 }
585 }
586
586
587 .radios {
587 .radios {
588 position: relative;
588 position: relative;
589 width: 505px;
589 width: 505px;
590 }
590 }
591 }
591 }
592
592
593 //--- MODULES ------------------//
593 //--- MODULES ------------------//
594
594
595
595
596 // Server Announcement
596 // Server Announcement
597 #server-announcement {
597 #server-announcement {
598 width: 95%;
598 width: 95%;
599 margin: @padding auto;
599 margin: @padding auto;
600 padding: @padding;
600 padding: @padding;
601 border-width: 2px;
601 border-width: 2px;
602 border-style: solid;
602 border-style: solid;
603 .border-radius(2px);
603 .border-radius(2px);
604 font-weight: @text-bold-weight;
604 font-weight: @text-bold-weight;
605 font-family: @text-bold;
605 font-family: @text-bold;
606
606
607 &.info { border-color: @alert4; background-color: @alert4-inner; }
607 &.info { border-color: @alert4; background-color: @alert4-inner; }
608 &.warning { border-color: @alert3; background-color: @alert3-inner; }
608 &.warning { border-color: @alert3; background-color: @alert3-inner; }
609 &.error { border-color: @alert2; background-color: @alert2-inner; }
609 &.error { border-color: @alert2; background-color: @alert2-inner; }
610 &.success { border-color: @alert1; background-color: @alert1-inner; }
610 &.success { border-color: @alert1; background-color: @alert1-inner; }
611 &.neutral { border-color: @grey3; background-color: @grey6; }
611 &.neutral { border-color: @grey3; background-color: @grey6; }
612 }
612 }
613
613
614 // Fixed Sidebar Column
614 // Fixed Sidebar Column
615 .sidebar-col-wrapper {
615 .sidebar-col-wrapper {
616 padding-left: @sidebar-all-width;
616 padding-left: @sidebar-all-width;
617
617
618 .sidebar {
618 .sidebar {
619 width: @sidebar-width;
619 width: @sidebar-width;
620 margin-left: -@sidebar-all-width;
620 margin-left: -@sidebar-all-width;
621 }
621 }
622 }
622 }
623
623
624 .sidebar-col-wrapper.scw-small {
624 .sidebar-col-wrapper.scw-small {
625 padding-left: @sidebar-small-all-width;
625 padding-left: @sidebar-small-all-width;
626
626
627 .sidebar {
627 .sidebar {
628 width: @sidebar-small-width;
628 width: @sidebar-small-width;
629 margin-left: -@sidebar-small-all-width;
629 margin-left: -@sidebar-small-all-width;
630 }
630 }
631 }
631 }
632
632
633
633
634 // FOOTER
634 // FOOTER
635 #footer {
635 #footer {
636 padding: 0;
636 padding: 0;
637 text-align: center;
637 text-align: center;
638 vertical-align: middle;
638 vertical-align: middle;
639 color: @grey2;
639 color: @grey2;
640 font-size: 11px;
640 font-size: 11px;
641
641
642 p {
642 p {
643 margin: 0;
643 margin: 0;
644 padding: 1em;
644 padding: 1em;
645 line-height: 1em;
645 line-height: 1em;
646 }
646 }
647
647
648 .server-instance { //server instance
648 .server-instance { //server instance
649 display: none;
649 display: none;
650 }
650 }
651
651
652 .title {
652 .title {
653 float: none;
653 float: none;
654 margin: 0 auto;
654 margin: 0 auto;
655 }
655 }
656 }
656 }
657
657
658 button.close {
658 button.close {
659 padding: 0;
659 padding: 0;
660 cursor: pointer;
660 cursor: pointer;
661 background: transparent;
661 background: transparent;
662 border: 0;
662 border: 0;
663 .box-shadow(none);
663 .box-shadow(none);
664 -webkit-appearance: none;
664 -webkit-appearance: none;
665 }
665 }
666
666
667 .close {
667 .close {
668 float: right;
668 float: right;
669 font-size: 21px;
669 font-size: 21px;
670 font-family: @text-bootstrap;
670 font-family: @text-bootstrap;
671 line-height: 1em;
671 line-height: 1em;
672 font-weight: bold;
672 font-weight: bold;
673 color: @grey2;
673 color: @grey2;
674
674
675 &:hover,
675 &:hover,
676 &:focus {
676 &:focus {
677 color: @grey1;
677 color: @grey1;
678 text-decoration: none;
678 text-decoration: none;
679 cursor: pointer;
679 cursor: pointer;
680 }
680 }
681 }
681 }
682
682
683 // GRID
683 // GRID
684 .sorting,
684 .sorting,
685 .sorting_desc,
685 .sorting_desc,
686 .sorting_asc {
686 .sorting_asc {
687 cursor: pointer;
687 cursor: pointer;
688 }
688 }
689 .sorting_desc:after {
689 .sorting_desc:after {
690 content: "\00A0\25B2";
690 content: "\00A0\25B2";
691 font-size: .75em;
691 font-size: .75em;
692 }
692 }
693 .sorting_asc:after {
693 .sorting_asc:after {
694 content: "\00A0\25BC";
694 content: "\00A0\25BC";
695 font-size: .68em;
695 font-size: .68em;
696 }
696 }
697
697
698
698
699 .user_auth_tokens {
699 .user_auth_tokens {
700
700
701 &.truncate {
701 &.truncate {
702 white-space: nowrap;
702 white-space: nowrap;
703 overflow: hidden;
703 overflow: hidden;
704 text-overflow: ellipsis;
704 text-overflow: ellipsis;
705 }
705 }
706
706
707 .fields .field .input {
707 .fields .field .input {
708 margin: 0;
708 margin: 0;
709 }
709 }
710
710
711 input#description {
711 input#description {
712 width: 100px;
712 width: 100px;
713 margin: 0;
713 margin: 0;
714 }
714 }
715
715
716 .drop-menu {
716 .drop-menu {
717 // TODO: johbo: Remove this, should work out of the box when
717 // TODO: johbo: Remove this, should work out of the box when
718 // having multiple inputs inline
718 // having multiple inputs inline
719 margin: 0 0 0 5px;
719 margin: 0 0 0 5px;
720 }
720 }
721 }
721 }
722 #user_list_table {
722 #user_list_table {
723 .closed {
723 .closed {
724 background-color: @grey6;
724 background-color: @grey6;
725 }
725 }
726 }
726 }
727
727
728
728
729 input, textarea {
729 input, textarea {
730 &.disabled {
730 &.disabled {
731 opacity: .5;
731 opacity: .5;
732 }
732 }
733
733
734 &:hover {
734 &:hover {
735 border-color: @grey3;
735 border-color: @grey3;
736 box-shadow: @button-shadow;
736 box-shadow: @button-shadow;
737 }
737 }
738
738
739 &:focus {
739 &:focus {
740 border-color: @rcblue;
740 border-color: @rcblue;
741 box-shadow: @button-shadow;
741 box-shadow: @button-shadow;
742 }
742 }
743 }
743 }
744
744
745 // remove extra padding in firefox
745 // remove extra padding in firefox
746 input::-moz-focus-inner { border:0; padding:0 }
746 input::-moz-focus-inner { border:0; padding:0 }
747
747
748 .adjacent input {
748 .adjacent input {
749 margin-bottom: @padding;
749 margin-bottom: @padding;
750 }
750 }
751
751
752 .permissions_boxes {
752 .permissions_boxes {
753 display: block;
753 display: block;
754 }
754 }
755
755
756 //FORMS
756 //FORMS
757
757
758 .medium-inline,
758 .medium-inline,
759 input#description.medium-inline {
759 input#description.medium-inline {
760 display: inline;
760 display: inline;
761 width: @medium-inline-input-width;
761 width: @medium-inline-input-width;
762 min-width: 100px;
762 min-width: 100px;
763 }
763 }
764
764
765 select {
765 select {
766 //reset
766 //reset
767 -webkit-appearance: none;
767 -webkit-appearance: none;
768 -moz-appearance: none;
768 -moz-appearance: none;
769
769
770 display: inline-block;
770 display: inline-block;
771 height: 28px;
771 height: 28px;
772 width: auto;
772 width: auto;
773 margin: 0 @padding @padding 0;
773 margin: 0 @padding @padding 0;
774 padding: 0 18px 0 8px;
774 padding: 0 18px 0 8px;
775 line-height:1em;
775 line-height:1em;
776 font-size: @basefontsize;
776 font-size: @basefontsize;
777 border: @border-thickness solid @grey5;
777 border: @border-thickness solid @grey5;
778 border-radius: @border-radius;
778 border-radius: @border-radius;
779 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
779 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
780 color: @grey4;
780 color: @grey4;
781 box-shadow: @button-shadow;
781 box-shadow: @button-shadow;
782
782
783 &:after {
783 &:after {
784 content: "\00A0\25BE";
784 content: "\00A0\25BE";
785 }
785 }
786
786
787 &:focus, &:hover {
787 &:focus, &:hover {
788 outline: none;
788 outline: none;
789 border-color: @grey4;
789 border-color: @grey4;
790 color: @rcdarkblue;
790 color: @rcdarkblue;
791 }
791 }
792 }
792 }
793
793
794 option {
794 option {
795 &:focus {
795 &:focus {
796 outline: none;
796 outline: none;
797 }
797 }
798 }
798 }
799
799
800 input,
800 input,
801 textarea {
801 textarea {
802 padding: @input-padding;
802 padding: @input-padding;
803 border: @input-border-thickness solid @border-highlight-color;
803 border: @input-border-thickness solid @border-highlight-color;
804 .border-radius (@border-radius);
804 .border-radius (@border-radius);
805 font-family: @text-light;
805 font-family: @text-light;
806 font-size: @basefontsize;
806 font-size: @basefontsize;
807
807
808 &.input-sm {
808 &.input-sm {
809 padding: 5px;
809 padding: 5px;
810 }
810 }
811
811
812 &#description {
812 &#description {
813 min-width: @input-description-minwidth;
813 min-width: @input-description-minwidth;
814 min-height: 1em;
814 min-height: 1em;
815 padding: 10px;
815 padding: 10px;
816 }
816 }
817 }
817 }
818
818
819 .field-sm {
819 .field-sm {
820 input,
820 input,
821 textarea {
821 textarea {
822 padding: 5px;
822 padding: 5px;
823 }
823 }
824 }
824 }
825
825
826 textarea {
826 textarea {
827 display: block;
827 display: block;
828 clear: both;
828 clear: both;
829 width: 100%;
829 width: 100%;
830 min-height: 100px;
830 min-height: 100px;
831 margin-bottom: @padding;
831 margin-bottom: @padding;
832 .box-sizing(border-box);
832 .box-sizing(border-box);
833 overflow: auto;
833 overflow: auto;
834 }
834 }
835
835
836 label {
836 label {
837 font-family: @text-light;
837 font-family: @text-light;
838 }
838 }
839
839
840 // GRAVATARS
840 // GRAVATARS
841 // centers gravatar on username to the right
841 // centers gravatar on username to the right
842
842
843 .gravatar {
843 .gravatar {
844 display: inline;
844 display: inline;
845 min-width: 16px;
845 min-width: 16px;
846 min-height: 16px;
846 min-height: 16px;
847 margin: -5px 0;
847 margin: -5px 0;
848 padding: 0;
848 padding: 0;
849 line-height: 1em;
849 line-height: 1em;
850 box-sizing: content-box;
850 box-sizing: content-box;
851 border-radius: 50%;
851 border-radius: 50%;
852
852
853 &.gravatar-large {
853 &.gravatar-large {
854 margin: -0.5em .25em -0.5em 0;
854 margin: -0.5em .25em -0.5em 0;
855 }
855 }
856
856
857 & + .user {
857 & + .user {
858 display: inline;
858 display: inline;
859 margin: 0;
859 margin: 0;
860 padding: 0 0 0 .17em;
860 padding: 0 0 0 .17em;
861 line-height: 1em;
861 line-height: 1em;
862 }
862 }
863
863
864 & + .no-margin {
864 & + .no-margin {
865 margin: 0
865 margin: 0
866 }
866 }
867
867
868 }
868 }
869
869
870 .user-inline-data {
870 .user-inline-data {
871 display: inline-block;
871 display: inline-block;
872 float: left;
872 float: left;
873 padding-left: .5em;
873 padding-left: .5em;
874 line-height: 1.3em;
874 line-height: 1.3em;
875 }
875 }
876
876
877 .rc-user { // gravatar + user wrapper
877 .rc-user { // gravatar + user wrapper
878 float: left;
878 float: left;
879 position: relative;
879 position: relative;
880 min-width: 100px;
880 min-width: 100px;
881 max-width: 200px;
881 max-width: 200px;
882 min-height: (@gravatar-size + @border-thickness * 2); // account for border
882 min-height: (@gravatar-size + @border-thickness * 2); // account for border
883 display: block;
883 display: block;
884 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
884 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
885
885
886
886
887 .gravatar {
887 .gravatar {
888 display: block;
888 display: block;
889 position: absolute;
889 position: absolute;
890 top: 0;
890 top: 0;
891 left: 0;
891 left: 0;
892 min-width: @gravatar-size;
892 min-width: @gravatar-size;
893 min-height: @gravatar-size;
893 min-height: @gravatar-size;
894 margin: 0;
894 margin: 0;
895 }
895 }
896
896
897 .user {
897 .user {
898 display: block;
898 display: block;
899 max-width: 175px;
899 max-width: 175px;
900 padding-top: 2px;
900 padding-top: 2px;
901 overflow: hidden;
901 overflow: hidden;
902 text-overflow: ellipsis;
902 text-overflow: ellipsis;
903 }
903 }
904 }
904 }
905
905
906 .gist-gravatar,
906 .gist-gravatar,
907 .journal_container {
907 .journal_container {
908 .gravatar-large {
908 .gravatar-large {
909 margin: 0 .5em -10px 0;
909 margin: 0 .5em -10px 0;
910 }
910 }
911 }
911 }
912
912
913 .gist-type-fields {
913 .gist-type-fields {
914 line-height: 30px;
914 line-height: 30px;
915 height: 30px;
915 height: 30px;
916
916
917 .gist-type-fields-wrapper {
917 .gist-type-fields-wrapper {
918 vertical-align: middle;
918 vertical-align: middle;
919 display: inline-block;
919 display: inline-block;
920 line-height: 25px;
920 line-height: 25px;
921 }
921 }
922 }
922 }
923
923
924 // ADMIN SETTINGS
924 // ADMIN SETTINGS
925
925
926 // Tag Patterns
926 // Tag Patterns
927 .tag_patterns {
927 .tag_patterns {
928 .tag_input {
928 .tag_input {
929 margin-bottom: @padding;
929 margin-bottom: @padding;
930 }
930 }
931 }
931 }
932
932
933 .locked_input {
933 .locked_input {
934 position: relative;
934 position: relative;
935
935
936 input {
936 input {
937 display: inline;
937 display: inline;
938 margin: 3px 5px 0px 0px;
938 margin: 3px 5px 0px 0px;
939 }
939 }
940
940
941 br {
941 br {
942 display: none;
942 display: none;
943 }
943 }
944
944
945 .error-message {
945 .error-message {
946 float: left;
946 float: left;
947 width: 100%;
947 width: 100%;
948 }
948 }
949
949
950 .lock_input_button {
950 .lock_input_button {
951 display: inline;
951 display: inline;
952 }
952 }
953
953
954 .help-block {
954 .help-block {
955 clear: both;
955 clear: both;
956 }
956 }
957 }
957 }
958
958
959 // Notifications
959 // Notifications
960
960
961 .notifications_buttons {
961 .notifications_buttons {
962 margin: 0 0 @space 0;
962 margin: 0 0 @space 0;
963 padding: 0;
963 padding: 0;
964
964
965 .btn {
965 .btn {
966 display: inline-block;
966 display: inline-block;
967 }
967 }
968 }
968 }
969
969
970 .notification-list {
970 .notification-list {
971
971
972 div {
972 div {
973 vertical-align: middle;
973 vertical-align: middle;
974 }
974 }
975
975
976 .container {
976 .container {
977 display: block;
977 display: block;
978 margin: 0 0 @padding 0;
978 margin: 0 0 @padding 0;
979 }
979 }
980
980
981 .delete-notifications {
981 .delete-notifications {
982 margin-left: @padding;
982 margin-left: @padding;
983 text-align: right;
983 text-align: right;
984 cursor: pointer;
984 cursor: pointer;
985 }
985 }
986
986
987 .read-notifications {
987 .read-notifications {
988 margin-left: @padding/2;
988 margin-left: @padding/2;
989 text-align: right;
989 text-align: right;
990 width: 35px;
990 width: 35px;
991 cursor: pointer;
991 cursor: pointer;
992 }
992 }
993
993
994 .icon-minus-sign {
994 .icon-minus-sign {
995 color: @alert2;
995 color: @alert2;
996 }
996 }
997
997
998 .icon-ok-sign {
998 .icon-ok-sign {
999 color: @alert1;
999 color: @alert1;
1000 }
1000 }
1001 }
1001 }
1002
1002
1003 .user_settings {
1003 .user_settings {
1004 float: left;
1004 float: left;
1005 clear: both;
1005 clear: both;
1006 display: block;
1006 display: block;
1007 width: 100%;
1007 width: 100%;
1008
1008
1009 .gravatar_box {
1009 .gravatar_box {
1010 margin-bottom: @padding;
1010 margin-bottom: @padding;
1011
1011
1012 &:after {
1012 &:after {
1013 content: " ";
1013 content: " ";
1014 clear: both;
1014 clear: both;
1015 width: 100%;
1015 width: 100%;
1016 }
1016 }
1017 }
1017 }
1018
1018
1019 .fields .field {
1019 .fields .field {
1020 clear: both;
1020 clear: both;
1021 }
1021 }
1022 }
1022 }
1023
1023
1024 .advanced_settings {
1024 .advanced_settings {
1025 margin-bottom: @space;
1025 margin-bottom: @space;
1026
1026
1027 .help-block {
1027 .help-block {
1028 margin-left: 0;
1028 margin-left: 0;
1029 }
1029 }
1030
1030
1031 button + .help-block {
1031 button + .help-block {
1032 margin-top: @padding;
1032 margin-top: @padding;
1033 }
1033 }
1034 }
1034 }
1035
1035
1036 // admin settings radio buttons and labels
1036 // admin settings radio buttons and labels
1037 .label-2 {
1037 .label-2 {
1038 float: left;
1038 float: left;
1039 width: @label2-width;
1039 width: @label2-width;
1040
1040
1041 label {
1041 label {
1042 color: @grey1;
1042 color: @grey1;
1043 }
1043 }
1044 }
1044 }
1045 .checkboxes {
1045 .checkboxes {
1046 float: left;
1046 float: left;
1047 width: @checkboxes-width;
1047 width: @checkboxes-width;
1048 margin-bottom: @padding;
1048 margin-bottom: @padding;
1049
1049
1050 .checkbox {
1050 .checkbox {
1051 width: 100%;
1051 width: 100%;
1052
1052
1053 label {
1053 label {
1054 margin: 0;
1054 margin: 0;
1055 padding: 0;
1055 padding: 0;
1056 }
1056 }
1057 }
1057 }
1058
1058
1059 .checkbox + .checkbox {
1059 .checkbox + .checkbox {
1060 display: inline-block;
1060 display: inline-block;
1061 }
1061 }
1062
1062
1063 label {
1063 label {
1064 margin-right: 1em;
1064 margin-right: 1em;
1065 }
1065 }
1066 }
1066 }
1067
1067
1068 // CHANGELOG
1068 // CHANGELOG
1069 .container_header {
1069 .container_header {
1070 float: left;
1070 float: left;
1071 display: block;
1071 display: block;
1072 width: 100%;
1072 width: 100%;
1073 margin: @padding 0 @padding;
1073 margin: @padding 0 @padding;
1074
1074
1075 #filter_changelog {
1075 #filter_changelog {
1076 float: left;
1076 float: left;
1077 margin-right: @padding;
1077 margin-right: @padding;
1078 }
1078 }
1079
1079
1080 .breadcrumbs_light {
1080 .breadcrumbs_light {
1081 display: inline-block;
1081 display: inline-block;
1082 }
1082 }
1083 }
1083 }
1084
1084
1085 .info_box {
1085 .info_box {
1086 float: right;
1086 float: right;
1087 }
1087 }
1088
1088
1089
1089
1090
1090
1091 #graph_content{
1091 #graph_content{
1092
1092
1093 // adjust for table headers so that graph renders properly
1093 // adjust for table headers so that graph renders properly
1094 // #graph_nodes padding - table cell padding
1094 // #graph_nodes padding - table cell padding
1095 padding-top: (@space - (@basefontsize * 2.4));
1095 padding-top: (@space - (@basefontsize * 2.4));
1096
1096
1097 &.graph_full_width {
1097 &.graph_full_width {
1098 width: 100%;
1098 width: 100%;
1099 max-width: 100%;
1099 max-width: 100%;
1100 }
1100 }
1101 }
1101 }
1102
1102
1103 #graph {
1103 #graph {
1104
1104
1105 .pagination-left {
1105 .pagination-left {
1106 float: left;
1106 float: left;
1107 clear: both;
1107 clear: both;
1108 }
1108 }
1109
1109
1110 .log-container {
1110 .log-container {
1111 max-width: 345px;
1111 max-width: 345px;
1112
1112
1113 .message{
1113 .message{
1114 max-width: 340px;
1114 max-width: 340px;
1115 }
1115 }
1116 }
1116 }
1117
1117
1118 .graph-col-wrapper {
1118 .graph-col-wrapper {
1119
1119
1120 #graph_nodes {
1120 #graph_nodes {
1121 width: 100px;
1121 width: 100px;
1122 position: absolute;
1122 position: absolute;
1123 left: 70px;
1123 left: 70px;
1124 z-index: -1;
1124 z-index: -1;
1125 }
1125 }
1126 }
1126 }
1127
1127
1128 .load-more-commits {
1128 .load-more-commits {
1129 text-align: center;
1129 text-align: center;
1130 }
1130 }
1131 .load-more-commits:hover {
1131 .load-more-commits:hover {
1132 background-color: @grey7;
1132 background-color: @grey7;
1133 }
1133 }
1134 .load-more-commits {
1134 .load-more-commits {
1135 a {
1135 a {
1136 display: block;
1136 display: block;
1137 }
1137 }
1138 }
1138 }
1139 }
1139 }
1140
1140
1141 .obsolete-toggle {
1141 .obsolete-toggle {
1142 line-height: 30px;
1142 line-height: 30px;
1143 margin-left: -15px;
1143 margin-left: -15px;
1144 }
1144 }
1145
1145
1146 #rev_range_container, #rev_range_clear, #rev_range_more {
1146 #rev_range_container, #rev_range_clear, #rev_range_more {
1147 margin-top: -5px;
1147 margin-top: -5px;
1148 margin-bottom: -5px;
1148 margin-bottom: -5px;
1149 }
1149 }
1150
1150
1151 #filter_changelog {
1151 #filter_changelog {
1152 float: left;
1152 float: left;
1153 }
1153 }
1154
1154
1155
1155
1156 //--- THEME ------------------//
1156 //--- THEME ------------------//
1157
1157
1158 #logo {
1158 #logo {
1159 float: left;
1159 float: left;
1160 margin: 9px 0 0 0;
1160 margin: 9px 0 0 0;
1161
1161
1162 .header {
1162 .header {
1163 background-color: transparent;
1163 background-color: transparent;
1164 }
1164 }
1165
1165
1166 a {
1166 a {
1167 display: inline-block;
1167 display: inline-block;
1168 }
1168 }
1169
1169
1170 img {
1170 img {
1171 height:30px;
1171 height:30px;
1172 }
1172 }
1173 }
1173 }
1174
1174
1175 .logo-wrapper {
1175 .logo-wrapper {
1176 float:left;
1176 float:left;
1177 }
1177 }
1178
1178
1179 .branding {
1179 .branding {
1180 float: left;
1180 float: left;
1181 padding: 9px 2px;
1181 padding: 9px 2px;
1182 line-height: 1em;
1182 line-height: 1em;
1183 font-size: @navigation-fontsize;
1183 font-size: @navigation-fontsize;
1184
1184
1185 a {
1185 a {
1186 color: @grey5
1186 color: @grey5
1187 }
1187 }
1188
1188
1189 // 1024px or smaller
1189 // 1024px or smaller
1190 @media screen and (max-width: 1180px) {
1190 @media screen and (max-width: 1180px) {
1191 display: none;
1191 display: none;
1192 }
1192 }
1193
1193
1194 }
1194 }
1195
1195
1196 img {
1196 img {
1197 border: none;
1197 border: none;
1198 outline: none;
1198 outline: none;
1199 }
1199 }
1200 user-profile-header
1200 user-profile-header
1201 label {
1201 label {
1202
1202
1203 input[type="checkbox"] {
1203 input[type="checkbox"] {
1204 margin-right: 1em;
1204 margin-right: 1em;
1205 }
1205 }
1206 input[type="radio"] {
1206 input[type="radio"] {
1207 margin-right: 1em;
1207 margin-right: 1em;
1208 }
1208 }
1209 }
1209 }
1210
1210
1211 .review-status {
1211 .review-status {
1212 &.under_review {
1212 &.under_review {
1213 color: @alert3;
1213 color: @alert3;
1214 }
1214 }
1215 &.approved {
1215 &.approved {
1216 color: @alert1;
1216 color: @alert1;
1217 }
1217 }
1218 &.rejected,
1218 &.rejected,
1219 &.forced_closed{
1219 &.forced_closed{
1220 color: @alert2;
1220 color: @alert2;
1221 }
1221 }
1222 &.not_reviewed {
1222 &.not_reviewed {
1223 color: @grey5;
1223 color: @grey5;
1224 }
1224 }
1225 }
1225 }
1226
1226
1227 .review-status-under_review {
1227 .review-status-under_review {
1228 color: @alert3;
1228 color: @alert3;
1229 }
1229 }
1230 .status-tag-under_review {
1230 .status-tag-under_review {
1231 border-color: @alert3;
1231 border-color: @alert3;
1232 }
1232 }
1233
1233
1234 .review-status-approved {
1234 .review-status-approved {
1235 color: @alert1;
1235 color: @alert1;
1236 }
1236 }
1237 .status-tag-approved {
1237 .status-tag-approved {
1238 border-color: @alert1;
1238 border-color: @alert1;
1239 }
1239 }
1240
1240
1241 .review-status-rejected,
1241 .review-status-rejected,
1242 .review-status-forced_closed {
1242 .review-status-forced_closed {
1243 color: @alert2;
1243 color: @alert2;
1244 }
1244 }
1245 .status-tag-rejected,
1245 .status-tag-rejected,
1246 .status-tag-forced_closed {
1246 .status-tag-forced_closed {
1247 border-color: @alert2;
1247 border-color: @alert2;
1248 }
1248 }
1249
1249
1250 .review-status-not_reviewed {
1250 .review-status-not_reviewed {
1251 color: @grey5;
1251 color: @grey5;
1252 }
1252 }
1253 .status-tag-not_reviewed {
1253 .status-tag-not_reviewed {
1254 border-color: @grey5;
1254 border-color: @grey5;
1255 }
1255 }
1256
1256
1257 .test_pattern_preview {
1257 .test_pattern_preview {
1258 margin: @space 0;
1258 margin: @space 0;
1259
1259
1260 p {
1260 p {
1261 margin-bottom: 0;
1261 margin-bottom: 0;
1262 border-bottom: @border-thickness solid @border-default-color;
1262 border-bottom: @border-thickness solid @border-default-color;
1263 color: @grey3;
1263 color: @grey3;
1264 }
1264 }
1265
1265
1266 .btn {
1266 .btn {
1267 margin-bottom: @padding;
1267 margin-bottom: @padding;
1268 }
1268 }
1269 }
1269 }
1270 #test_pattern_result {
1270 #test_pattern_result {
1271 display: none;
1271 display: none;
1272 &:extend(pre);
1272 &:extend(pre);
1273 padding: .9em;
1273 padding: .9em;
1274 color: @grey3;
1274 color: @grey3;
1275 background-color: @grey7;
1275 background-color: @grey7;
1276 border-right: @border-thickness solid @border-default-color;
1276 border-right: @border-thickness solid @border-default-color;
1277 border-bottom: @border-thickness solid @border-default-color;
1277 border-bottom: @border-thickness solid @border-default-color;
1278 border-left: @border-thickness solid @border-default-color;
1278 border-left: @border-thickness solid @border-default-color;
1279 }
1279 }
1280
1280
1281 #repo_vcs_settings {
1281 #repo_vcs_settings {
1282 #inherit_overlay_vcs_default {
1282 #inherit_overlay_vcs_default {
1283 display: none;
1283 display: none;
1284 }
1284 }
1285 #inherit_overlay_vcs_custom {
1285 #inherit_overlay_vcs_custom {
1286 display: custom;
1286 display: custom;
1287 }
1287 }
1288 &.inherited {
1288 &.inherited {
1289 #inherit_overlay_vcs_default {
1289 #inherit_overlay_vcs_default {
1290 display: block;
1290 display: block;
1291 }
1291 }
1292 #inherit_overlay_vcs_custom {
1292 #inherit_overlay_vcs_custom {
1293 display: none;
1293 display: none;
1294 }
1294 }
1295 }
1295 }
1296 }
1296 }
1297
1297
1298 .issue-tracker-link {
1298 .issue-tracker-link {
1299 color: @rcblue;
1299 color: @rcblue;
1300 }
1300 }
1301
1301
1302 // Issue Tracker Table Show/Hide
1302 // Issue Tracker Table Show/Hide
1303 #repo_issue_tracker {
1303 #repo_issue_tracker {
1304 #inherit_overlay {
1304 #inherit_overlay {
1305 display: none;
1305 display: none;
1306 }
1306 }
1307 #custom_overlay {
1307 #custom_overlay {
1308 display: custom;
1308 display: custom;
1309 }
1309 }
1310 &.inherited {
1310 &.inherited {
1311 #inherit_overlay {
1311 #inherit_overlay {
1312 display: block;
1312 display: block;
1313 }
1313 }
1314 #custom_overlay {
1314 #custom_overlay {
1315 display: none;
1315 display: none;
1316 }
1316 }
1317 }
1317 }
1318 }
1318 }
1319 table.issuetracker {
1319 table.issuetracker {
1320 &.readonly {
1320 &.readonly {
1321 tr, td {
1321 tr, td {
1322 color: @grey3;
1322 color: @grey3;
1323 }
1323 }
1324 }
1324 }
1325 .edit {
1325 .edit {
1326 display: none;
1326 display: none;
1327 }
1327 }
1328 .editopen {
1328 .editopen {
1329 .edit {
1329 .edit {
1330 display: inline;
1330 display: inline;
1331 }
1331 }
1332 .entry {
1332 .entry {
1333 display: none;
1333 display: none;
1334 }
1334 }
1335 }
1335 }
1336 tr td.td-action {
1336 tr td.td-action {
1337 min-width: 117px;
1337 min-width: 117px;
1338 }
1338 }
1339 td input {
1339 td input {
1340 max-width: none;
1340 max-width: none;
1341 min-width: 30px;
1341 min-width: 30px;
1342 width: 80%;
1342 width: 80%;
1343 }
1343 }
1344 .issuetracker_pref input {
1344 .issuetracker_pref input {
1345 width: 40%;
1345 width: 40%;
1346 }
1346 }
1347 input.edit_issuetracker_update {
1347 input.edit_issuetracker_update {
1348 margin-right: 0;
1348 margin-right: 0;
1349 width: auto;
1349 width: auto;
1350 }
1350 }
1351 }
1351 }
1352
1352
1353 table.integrations {
1353 table.integrations {
1354 .td-icon {
1354 .td-icon {
1355 width: 20px;
1355 width: 20px;
1356 .integration-icon {
1356 .integration-icon {
1357 height: 20px;
1357 height: 20px;
1358 width: 20px;
1358 width: 20px;
1359 }
1359 }
1360 }
1360 }
1361 }
1361 }
1362
1362
1363 .integrations {
1363 .integrations {
1364 a.integration-box {
1364 a.integration-box {
1365 color: @text-color;
1365 color: @text-color;
1366 &:hover {
1366 &:hover {
1367 .panel {
1367 .panel {
1368 background: #fbfbfb;
1368 background: #fbfbfb;
1369 }
1369 }
1370 }
1370 }
1371 .integration-icon {
1371 .integration-icon {
1372 width: 30px;
1372 width: 30px;
1373 height: 30px;
1373 height: 30px;
1374 margin-right: 20px;
1374 margin-right: 20px;
1375 float: left;
1375 float: left;
1376 }
1376 }
1377
1377
1378 .panel-body {
1378 .panel-body {
1379 padding: 10px;
1379 padding: 10px;
1380 }
1380 }
1381 .panel {
1381 .panel {
1382 margin-bottom: 10px;
1382 margin-bottom: 10px;
1383 }
1383 }
1384 h2 {
1384 h2 {
1385 display: inline-block;
1385 display: inline-block;
1386 margin: 0;
1386 margin: 0;
1387 min-width: 140px;
1387 min-width: 140px;
1388 }
1388 }
1389 }
1389 }
1390 a.integration-box.dummy-integration {
1390 a.integration-box.dummy-integration {
1391 color: @grey4
1391 color: @grey4
1392 }
1392 }
1393 }
1393 }
1394
1394
1395 //Permissions Settings
1395 //Permissions Settings
1396 #add_perm {
1396 #add_perm {
1397 margin: 0 0 @padding;
1397 margin: 0 0 @padding;
1398 cursor: pointer;
1398 cursor: pointer;
1399 }
1399 }
1400
1400
1401 .perm_ac {
1401 .perm_ac {
1402 input {
1402 input {
1403 width: 95%;
1403 width: 95%;
1404 }
1404 }
1405 }
1405 }
1406
1406
1407 .autocomplete-suggestions {
1407 .autocomplete-suggestions {
1408 width: auto !important; // overrides autocomplete.js
1408 width: auto !important; // overrides autocomplete.js
1409 min-width: 278px;
1409 min-width: 278px;
1410 margin: 0;
1410 margin: 0;
1411 border: @border-thickness solid @grey5;
1411 border: @border-thickness solid @grey5;
1412 border-radius: @border-radius;
1412 border-radius: @border-radius;
1413 color: @grey2;
1413 color: @grey2;
1414 background-color: white;
1414 background-color: white;
1415 }
1415 }
1416
1416
1417 .autocomplete-qfilter-suggestions {
1417 .autocomplete-qfilter-suggestions {
1418 width: auto !important; // overrides autocomplete.js
1418 width: auto !important; // overrides autocomplete.js
1419 max-height: 100% !important;
1419 max-height: 100% !important;
1420 min-width: 376px;
1420 min-width: 376px;
1421 margin: 0;
1421 margin: 0;
1422 border: @border-thickness solid @grey5;
1422 border: @border-thickness solid @grey5;
1423 color: @grey2;
1423 color: @grey2;
1424 background-color: white;
1424 background-color: white;
1425 }
1425 }
1426
1426
1427 .autocomplete-selected {
1427 .autocomplete-selected {
1428 background: #F0F0F0;
1428 background: #F0F0F0;
1429 }
1429 }
1430
1430
1431 .ac-container-wrap {
1431 .ac-container-wrap {
1432 margin: 0;
1432 margin: 0;
1433 padding: 8px;
1433 padding: 8px;
1434 border-bottom: @border-thickness solid @grey5;
1434 border-bottom: @border-thickness solid @grey5;
1435 list-style-type: none;
1435 list-style-type: none;
1436 cursor: pointer;
1436 cursor: pointer;
1437
1437
1438 &:hover {
1438 &:hover {
1439 background-color: @grey7;
1439 background-color: @grey7;
1440 }
1440 }
1441
1441
1442 img {
1442 img {
1443 height: @gravatar-size;
1443 height: @gravatar-size;
1444 width: @gravatar-size;
1444 width: @gravatar-size;
1445 margin-right: 1em;
1445 margin-right: 1em;
1446 }
1446 }
1447
1447
1448 strong {
1448 strong {
1449 font-weight: normal;
1449 font-weight: normal;
1450 }
1450 }
1451 }
1451 }
1452
1452
1453 // Settings Dropdown
1453 // Settings Dropdown
1454 .user-menu .container {
1454 .user-menu .container {
1455 padding: 0 4px;
1455 padding: 0 4px;
1456 margin: 0;
1456 margin: 0;
1457 }
1457 }
1458
1458
1459 .user-menu .gravatar {
1459 .user-menu .gravatar {
1460 cursor: pointer;
1460 cursor: pointer;
1461 }
1461 }
1462
1462
1463 .codeblock {
1463 .codeblock {
1464 margin-bottom: @padding;
1464 margin-bottom: @padding;
1465 clear: both;
1465 clear: both;
1466
1466
1467 .stats {
1467 .stats {
1468 overflow: hidden;
1468 overflow: hidden;
1469 }
1469 }
1470
1470
1471 .message{
1471 .message{
1472 textarea{
1472 textarea{
1473 margin: 0;
1473 margin: 0;
1474 }
1474 }
1475 }
1475 }
1476
1476
1477 .code-header {
1477 .code-header {
1478 .stats {
1478 .stats {
1479 line-height: 2em;
1479 line-height: 2em;
1480
1480
1481 .revision_id {
1481 .revision_id {
1482 margin-left: 0;
1482 margin-left: 0;
1483 }
1483 }
1484 .buttons {
1484 .buttons {
1485 padding-right: 0;
1485 padding-right: 0;
1486 }
1486 }
1487 }
1487 }
1488
1488
1489 .item{
1489 .item{
1490 margin-right: 0.5em;
1490 margin-right: 0.5em;
1491 }
1491 }
1492 }
1492 }
1493
1493
1494 #editor_container {
1494 #editor_container {
1495 position: relative;
1495 position: relative;
1496 margin: @padding 10px;
1496 margin: @padding 10px;
1497 }
1497 }
1498 }
1498 }
1499
1499
1500 #file_history_container {
1500 #file_history_container {
1501 display: none;
1501 display: none;
1502 }
1502 }
1503
1503
1504 .file-history-inner {
1504 .file-history-inner {
1505 margin-bottom: 10px;
1505 margin-bottom: 10px;
1506 }
1506 }
1507
1507
1508 // Pull Requests
1508 // Pull Requests
1509 .summary-details {
1509 .summary-details {
1510 width: 100%;
1510 width: 100%;
1511 }
1511 }
1512 .pr-summary {
1512 .pr-summary {
1513 border-bottom: @border-thickness solid @grey5;
1513 border-bottom: @border-thickness solid @grey5;
1514 margin-bottom: @space;
1514 margin-bottom: @space;
1515 }
1515 }
1516
1516
1517 .reviewers {
1517 .reviewers {
1518 width: 98%;
1518 width: 98%;
1519 }
1519 }
1520
1520
1521 .reviewers ul li {
1521 .reviewers ul li {
1522 position: relative;
1522 position: relative;
1523 width: 100%;
1523 width: 100%;
1524 padding-bottom: 8px;
1524 padding-bottom: 8px;
1525 list-style-type: none;
1525 list-style-type: none;
1526 }
1526 }
1527
1527
1528 .reviewer_entry {
1528 .reviewer_entry {
1529 min-height: 55px;
1529 min-height: 55px;
1530 }
1530 }
1531
1531
1532 .reviewer_reason {
1532 .reviewer_reason {
1533 padding-left: 20px;
1533 padding-left: 20px;
1534 line-height: 1.5em;
1534 line-height: 1.5em;
1535 }
1535 }
1536 .reviewer_status {
1536 .reviewer_status {
1537 display: inline-block;
1537 display: inline-block;
1538 width: 20px;
1538 width: 20px;
1539 min-width: 20px;
1539 min-width: 20px;
1540 height: 1.2em;
1540 height: 1.2em;
1541 line-height: 1em;
1541 line-height: 1em;
1542 }
1542 }
1543
1543
1544 .reviewer_name {
1544 .reviewer_name {
1545 display: inline-block;
1545 display: inline-block;
1546 max-width: 83%;
1546 max-width: 83%;
1547 padding-right: 20px;
1547 padding-right: 20px;
1548 vertical-align: middle;
1548 vertical-align: middle;
1549 line-height: 1;
1549 line-height: 1;
1550
1550
1551 .rc-user {
1551 .rc-user {
1552 min-width: 0;
1552 min-width: 0;
1553 margin: -2px 1em 0 0;
1553 margin: -2px 1em 0 0;
1554 }
1554 }
1555
1555
1556 .reviewer {
1556 .reviewer {
1557 float: left;
1557 float: left;
1558 }
1558 }
1559 }
1559 }
1560
1560
1561 .reviewer_member_mandatory {
1561 .reviewer_member_mandatory {
1562 width: 16px;
1562 width: 16px;
1563 font-size: 11px;
1563 font-size: 11px;
1564 margin: 0;
1564 margin: 0;
1565 padding: 0;
1565 padding: 0;
1566 color: black;
1566 color: black;
1567 opacity: 0.4;
1567 opacity: 0.4;
1568 }
1568 }
1569
1569
1570 .reviewer_member_mandatory_remove,
1570 .reviewer_member_mandatory_remove,
1571 .reviewer_member_remove {
1571 .reviewer_member_remove {
1572 width: 16px;
1572 width: 16px;
1573 padding: 0;
1573 padding: 0;
1574 color: black;
1574 color: black;
1575 cursor: pointer;
1575 cursor: pointer;
1576 }
1576 }
1577
1577
1578 .reviewer_member_mandatory_remove {
1578 .reviewer_member_mandatory_remove {
1579 color: @grey4;
1579 color: @grey4;
1580 }
1580 }
1581
1581
1582 .reviewer_member_status {
1582 .reviewer_member_status {
1583 margin-top: 5px;
1583 margin-top: 5px;
1584 }
1584 }
1585 .pr-summary #summary{
1585 .pr-summary #summary{
1586 width: 100%;
1586 width: 100%;
1587 }
1587 }
1588 .pr-summary .action_button:hover {
1588 .pr-summary .action_button:hover {
1589 border: 0;
1589 border: 0;
1590 cursor: pointer;
1590 cursor: pointer;
1591 }
1591 }
1592 .pr-details-title {
1592 .pr-details-title {
1593 height: 20px;
1593 height: 20px;
1594 line-height: 16px;
1594 line-height: 16px;
1595
1595
1596 padding-bottom: 4px;
1596 padding-bottom: 4px;
1597 border-bottom: @border-thickness solid @grey5;
1597 border-bottom: @border-thickness solid @grey5;
1598
1598
1599 .action_button.disabled {
1599 .action_button.disabled {
1600 color: @grey4;
1600 color: @grey4;
1601 cursor: inherit;
1601 cursor: inherit;
1602 }
1602 }
1603 .action_button {
1603 .action_button {
1604 color: @rcblue;
1604 color: @rcblue;
1605 }
1605 }
1606 }
1606 }
1607 .pr-details-content {
1607 .pr-details-content {
1608 margin-top: @textmargin - 5;
1608 margin-top: @textmargin - 5;
1609 margin-bottom: @textmargin - 5;
1609 margin-bottom: @textmargin - 5;
1610 }
1610 }
1611
1611
1612 .pr-reviewer-rules {
1612 .pr-reviewer-rules {
1613 padding: 10px 0px 20px 0px;
1613 padding: 10px 0px 20px 0px;
1614 }
1614 }
1615
1615
1616 .todo-resolved {
1616 .todo-resolved {
1617 text-decoration: line-through;
1617 text-decoration: line-through;
1618 }
1618 }
1619
1619
1620 .todo-table, .comments-table {
1620 .todo-table, .comments-table {
1621 width: 100%;
1621 width: 100%;
1622
1622
1623 td {
1623 td {
1624 padding: 5px 0px;
1624 padding: 5px 0px;
1625 }
1625 }
1626
1626
1627 .td-todo-number {
1627 .td-todo-number {
1628 text-align: left;
1628 text-align: left;
1629 white-space: nowrap;
1629 white-space: nowrap;
1630 width: 1%;
1630 width: 1%;
1631 padding-right: 2px;
1631 padding-right: 2px;
1632 }
1632 }
1633
1633
1634 .td-todo-gravatar {
1634 .td-todo-gravatar {
1635 width: 5%;
1635 width: 5%;
1636
1636
1637 img {
1637 img {
1638 margin: -3px 0;
1638 margin: -3px 0;
1639 }
1639 }
1640 }
1640 }
1641
1641
1642 }
1642 }
1643
1643
1644 .todo-comment-text-wrapper {
1644 .todo-comment-text-wrapper {
1645 display: inline-grid;
1645 display: inline-grid;
1646 }
1646 }
1647
1647
1648 .todo-comment-text {
1648 .todo-comment-text {
1649 margin-left: 5px;
1649 margin-left: 5px;
1650 white-space: nowrap;
1650 white-space: nowrap;
1651 overflow: hidden;
1651 overflow: hidden;
1652 text-overflow: ellipsis;
1652 text-overflow: ellipsis;
1653 }
1653 }
1654
1654
1655 table.group_members {
1655 table.group_members {
1656 width: 100%
1656 width: 100%
1657 }
1657 }
1658
1658
1659 .group_members {
1659 .group_members {
1660 margin-top: 0;
1660 margin-top: 0;
1661 padding: 0;
1661 padding: 0;
1662
1662
1663 img {
1663 img {
1664 height: @gravatar-size;
1664 height: @gravatar-size;
1665 width: @gravatar-size;
1665 width: @gravatar-size;
1666 margin-right: .5em;
1666 margin-right: .5em;
1667 margin-left: 3px;
1667 margin-left: 3px;
1668 }
1668 }
1669
1669
1670 .to-delete {
1670 .to-delete {
1671 .user {
1671 .user {
1672 text-decoration: line-through;
1672 text-decoration: line-through;
1673 }
1673 }
1674 }
1674 }
1675 }
1675 }
1676
1676
1677 .compare_view_commits_title {
1677 .compare_view_commits_title {
1678 .disabled {
1678 .disabled {
1679 cursor: inherit;
1679 cursor: inherit;
1680 &:hover{
1680 &:hover{
1681 background-color: inherit;
1681 background-color: inherit;
1682 color: inherit;
1682 color: inherit;
1683 }
1683 }
1684 }
1684 }
1685 }
1685 }
1686
1686
1687 .subtitle-compare {
1687 .subtitle-compare {
1688 margin: -15px 0px 0px 0px;
1688 margin: -15px 0px 0px 0px;
1689 }
1689 }
1690
1690
1691 // new entry in group_members
1691 // new entry in group_members
1692 .td-author-new-entry {
1692 .td-author-new-entry {
1693 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1693 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1694 }
1694 }
1695
1695
1696 .usergroup_member_remove {
1696 .usergroup_member_remove {
1697 width: 16px;
1697 width: 16px;
1698 margin-bottom: 10px;
1698 margin-bottom: 10px;
1699 padding: 0;
1699 padding: 0;
1700 color: black !important;
1700 color: black !important;
1701 cursor: pointer;
1701 cursor: pointer;
1702 }
1702 }
1703
1703
1704 .reviewer_ac .ac-input {
1704 .reviewer_ac .ac-input {
1705 width: 98%;
1705 width: 98%;
1706 margin-bottom: 1em;
1706 margin-bottom: 1em;
1707 }
1707 }
1708
1708
1709 .observer_ac .ac-input {
1709 .observer_ac .ac-input {
1710 width: 98%;
1710 width: 98%;
1711 margin-bottom: 1em;
1711 margin-bottom: 1em;
1712 }
1712 }
1713
1713
1714 .rule-table {
1714 .rule-table {
1715 width: 100%;
1715 width: 100%;
1716 }
1716 }
1717
1717
1718 .rule-table td {
1718 .rule-table td {
1719
1719
1720 }
1720 }
1721
1721
1722 .rule-table .td-role {
1722 .rule-table .td-role {
1723 width: 100px
1723 width: 100px
1724 }
1724 }
1725
1725
1726 .rule-table .td-mandatory {
1726 .rule-table .td-mandatory {
1727 width: 100px
1727 width: 100px
1728 }
1728 }
1729
1729
1730 .rule-table .td-group-votes {
1730 .rule-table .td-group-votes {
1731 width: 150px
1731 width: 150px
1732 }
1732 }
1733
1733
1734 .compare_view_commits tr{
1734 .compare_view_commits tr{
1735 height: 20px;
1735 height: 20px;
1736 }
1736 }
1737 .compare_view_commits td {
1737 .compare_view_commits td {
1738 vertical-align: top;
1738 vertical-align: top;
1739 padding-top: 10px;
1739 padding-top: 10px;
1740 }
1740 }
1741 .compare_view_commits .author {
1741 .compare_view_commits .author {
1742 margin-left: 5px;
1742 margin-left: 5px;
1743 }
1743 }
1744
1744
1745 .compare_view_commits {
1745 .compare_view_commits {
1746 .color-a {
1746 .color-a {
1747 color: @alert1;
1747 color: @alert1;
1748 }
1748 }
1749
1749
1750 .color-c {
1750 .color-c {
1751 color: @color3;
1751 color: @color3;
1752 }
1752 }
1753
1753
1754 .color-r {
1754 .color-r {
1755 color: @color5;
1755 color: @color5;
1756 }
1756 }
1757
1757
1758 .color-a-bg {
1758 .color-a-bg {
1759 background-color: @alert1;
1759 background-color: @alert1;
1760 }
1760 }
1761
1761
1762 .color-c-bg {
1762 .color-c-bg {
1763 background-color: @alert3;
1763 background-color: @alert3;
1764 }
1764 }
1765
1765
1766 .color-r-bg {
1766 .color-r-bg {
1767 background-color: @alert2;
1767 background-color: @alert2;
1768 }
1768 }
1769
1769
1770 .color-a-border {
1770 .color-a-border {
1771 border: 1px solid @alert1;
1771 border: 1px solid @alert1;
1772 }
1772 }
1773
1773
1774 .color-c-border {
1774 .color-c-border {
1775 border: 1px solid @alert3;
1775 border: 1px solid @alert3;
1776 }
1776 }
1777
1777
1778 .color-r-border {
1778 .color-r-border {
1779 border: 1px solid @alert2;
1779 border: 1px solid @alert2;
1780 }
1780 }
1781
1781
1782 .commit-change-indicator {
1782 .commit-change-indicator {
1783 width: 15px;
1783 width: 15px;
1784 height: 15px;
1784 height: 15px;
1785 position: relative;
1785 position: relative;
1786 left: 15px;
1786 left: 15px;
1787 }
1787 }
1788
1788
1789 .commit-change-content {
1789 .commit-change-content {
1790 text-align: center;
1790 text-align: center;
1791 vertical-align: middle;
1791 vertical-align: middle;
1792 line-height: 15px;
1792 line-height: 15px;
1793 }
1793 }
1794 }
1794 }
1795
1795
1796 .compare_view_filepath {
1796 .compare_view_filepath {
1797 color: @grey1;
1797 color: @grey1;
1798 }
1798 }
1799
1799
1800 .show_more {
1800 .show_more {
1801 display: inline-block;
1801 display: inline-block;
1802 width: 0;
1802 width: 0;
1803 height: 0;
1803 height: 0;
1804 vertical-align: middle;
1804 vertical-align: middle;
1805 content: "";
1805 content: "";
1806 border: 4px solid;
1806 border: 4px solid;
1807 border-right-color: transparent;
1807 border-right-color: transparent;
1808 border-bottom-color: transparent;
1808 border-bottom-color: transparent;
1809 border-left-color: transparent;
1809 border-left-color: transparent;
1810 font-size: 0;
1810 font-size: 0;
1811 }
1811 }
1812
1812
1813 .journal_more .show_more {
1813 .journal_more .show_more {
1814 display: inline;
1814 display: inline;
1815
1815
1816 &:after {
1816 &:after {
1817 content: none;
1817 content: none;
1818 }
1818 }
1819 }
1819 }
1820
1820
1821 .compare_view_commits .collapse_commit:after {
1821 .compare_view_commits .collapse_commit:after {
1822 cursor: pointer;
1822 cursor: pointer;
1823 content: "\00A0\25B4";
1823 content: "\00A0\25B4";
1824 margin-left: -3px;
1824 margin-left: -3px;
1825 font-size: 17px;
1825 font-size: 17px;
1826 color: @grey4;
1826 color: @grey4;
1827 }
1827 }
1828
1828
1829 .diff_links {
1829 .diff_links {
1830 margin-left: 8px;
1830 margin-left: 8px;
1831 }
1831 }
1832
1832
1833 #pull_request_overview {
1833 #pull_request_overview {
1834 div.ancestor {
1834 div.ancestor {
1835 margin: -33px 0;
1835 margin: -33px 0;
1836 }
1836 }
1837 }
1837 }
1838
1838
1839 div.ancestor {
1839 div.ancestor {
1840
1840
1841 }
1841 }
1842
1842
1843 .cs_icon_td input[type="checkbox"] {
1843 .cs_icon_td input[type="checkbox"] {
1844 display: none;
1844 display: none;
1845 }
1845 }
1846
1846
1847 .cs_icon_td .expand_file_icon:after {
1847 .cs_icon_td .expand_file_icon:after {
1848 cursor: pointer;
1848 cursor: pointer;
1849 content: "\00A0\25B6";
1849 content: "\00A0\25B6";
1850 font-size: 12px;
1850 font-size: 12px;
1851 color: @grey4;
1851 color: @grey4;
1852 }
1852 }
1853
1853
1854 .cs_icon_td .collapse_file_icon:after {
1854 .cs_icon_td .collapse_file_icon:after {
1855 cursor: pointer;
1855 cursor: pointer;
1856 content: "\00A0\25BC";
1856 content: "\00A0\25BC";
1857 font-size: 12px;
1857 font-size: 12px;
1858 color: @grey4;
1858 color: @grey4;
1859 }
1859 }
1860
1860
1861 /*new binary
1861 /*new binary
1862 NEW_FILENODE = 1
1862 NEW_FILENODE = 1
1863 DEL_FILENODE = 2
1863 DEL_FILENODE = 2
1864 MOD_FILENODE = 3
1864 MOD_FILENODE = 3
1865 RENAMED_FILENODE = 4
1865 RENAMED_FILENODE = 4
1866 COPIED_FILENODE = 5
1866 COPIED_FILENODE = 5
1867 CHMOD_FILENODE = 6
1867 CHMOD_FILENODE = 6
1868 BIN_FILENODE = 7
1868 BIN_FILENODE = 7
1869 */
1869 */
1870 .cs_files_expand {
1870 .cs_files_expand {
1871 font-size: @basefontsize + 5px;
1871 font-size: @basefontsize + 5px;
1872 line-height: 1.8em;
1872 line-height: 1.8em;
1873 float: right;
1873 float: right;
1874 }
1874 }
1875
1875
1876 .cs_files_expand span{
1876 .cs_files_expand span{
1877 color: @rcblue;
1877 color: @rcblue;
1878 cursor: pointer;
1878 cursor: pointer;
1879 }
1879 }
1880 .cs_files {
1880 .cs_files {
1881 clear: both;
1881 clear: both;
1882 padding-bottom: @padding;
1882 padding-bottom: @padding;
1883
1883
1884 .cur_cs {
1884 .cur_cs {
1885 margin: 10px 2px;
1885 margin: 10px 2px;
1886 font-weight: bold;
1886 font-weight: bold;
1887 }
1887 }
1888
1888
1889 .node {
1889 .node {
1890 float: left;
1890 float: left;
1891 }
1891 }
1892
1892
1893 .changes {
1893 .changes {
1894 float: right;
1894 float: right;
1895 color: white;
1895 color: white;
1896 font-size: @basefontsize - 4px;
1896 font-size: @basefontsize - 4px;
1897 margin-top: 4px;
1897 margin-top: 4px;
1898 opacity: 0.6;
1898 opacity: 0.6;
1899 filter: Alpha(opacity=60); /* IE8 and earlier */
1899 filter: Alpha(opacity=60); /* IE8 and earlier */
1900
1900
1901 .added {
1901 .added {
1902 background-color: @alert1;
1902 background-color: @alert1;
1903 float: left;
1903 float: left;
1904 text-align: center;
1904 text-align: center;
1905 }
1905 }
1906
1906
1907 .deleted {
1907 .deleted {
1908 background-color: @alert2;
1908 background-color: @alert2;
1909 float: left;
1909 float: left;
1910 text-align: center;
1910 text-align: center;
1911 }
1911 }
1912
1912
1913 .bin {
1913 .bin {
1914 background-color: @alert1;
1914 background-color: @alert1;
1915 text-align: center;
1915 text-align: center;
1916 }
1916 }
1917
1917
1918 /*new binary*/
1918 /*new binary*/
1919 .bin.bin1 {
1919 .bin.bin1 {
1920 background-color: @alert1;
1920 background-color: @alert1;
1921 text-align: center;
1921 text-align: center;
1922 }
1922 }
1923
1923
1924 /*deleted binary*/
1924 /*deleted binary*/
1925 .bin.bin2 {
1925 .bin.bin2 {
1926 background-color: @alert2;
1926 background-color: @alert2;
1927 text-align: center;
1927 text-align: center;
1928 }
1928 }
1929
1929
1930 /*mod binary*/
1930 /*mod binary*/
1931 .bin.bin3 {
1931 .bin.bin3 {
1932 background-color: @grey2;
1932 background-color: @grey2;
1933 text-align: center;
1933 text-align: center;
1934 }
1934 }
1935
1935
1936 /*rename file*/
1936 /*rename file*/
1937 .bin.bin4 {
1937 .bin.bin4 {
1938 background-color: @alert4;
1938 background-color: @alert4;
1939 text-align: center;
1939 text-align: center;
1940 }
1940 }
1941
1941
1942 /*copied file*/
1942 /*copied file*/
1943 .bin.bin5 {
1943 .bin.bin5 {
1944 background-color: @alert4;
1944 background-color: @alert4;
1945 text-align: center;
1945 text-align: center;
1946 }
1946 }
1947
1947
1948 /*chmod file*/
1948 /*chmod file*/
1949 .bin.bin6 {
1949 .bin.bin6 {
1950 background-color: @grey2;
1950 background-color: @grey2;
1951 text-align: center;
1951 text-align: center;
1952 }
1952 }
1953 }
1953 }
1954 }
1954 }
1955
1955
1956 .cs_files .cs_added, .cs_files .cs_A,
1956 .cs_files .cs_added, .cs_files .cs_A,
1957 .cs_files .cs_added, .cs_files .cs_M,
1957 .cs_files .cs_added, .cs_files .cs_M,
1958 .cs_files .cs_added, .cs_files .cs_D {
1958 .cs_files .cs_added, .cs_files .cs_D {
1959 height: 16px;
1959 height: 16px;
1960 padding-right: 10px;
1960 padding-right: 10px;
1961 margin-top: 7px;
1961 margin-top: 7px;
1962 text-align: left;
1962 text-align: left;
1963 }
1963 }
1964
1964
1965 .cs_icon_td {
1965 .cs_icon_td {
1966 min-width: 16px;
1966 min-width: 16px;
1967 width: 16px;
1967 width: 16px;
1968 }
1968 }
1969
1969
1970 .pull-request-merge {
1970 .pull-request-merge {
1971 border: 1px solid @grey5;
1971 border: 1px solid @grey5;
1972 padding: 10px 0px 20px;
1972 padding: 10px 0px 20px;
1973 margin-top: 10px;
1973 margin-top: 10px;
1974 margin-bottom: 20px;
1974 margin-bottom: 20px;
1975 }
1975 }
1976
1976
1977 .pull-request-merge-refresh {
1977 .pull-request-merge-refresh {
1978 margin: 2px 7px;
1978 margin: 2px 7px;
1979 a {
1979 a {
1980 color: @grey3;
1980 color: @grey3;
1981 }
1981 }
1982 }
1982 }
1983
1983
1984 .pull-request-merge ul {
1984 .pull-request-merge ul {
1985 padding: 0px 0px;
1985 padding: 0px 0px;
1986 }
1986 }
1987
1987
1988 .pull-request-merge li {
1988 .pull-request-merge li {
1989 list-style-type: none;
1989 list-style-type: none;
1990 }
1990 }
1991
1991
1992 .pull-request-merge .pull-request-wrap {
1992 .pull-request-merge .pull-request-wrap {
1993 height: auto;
1993 height: auto;
1994 padding: 0px 0px;
1994 padding: 0px 0px;
1995 text-align: right;
1995 text-align: right;
1996 }
1996 }
1997
1997
1998 .pull-request-merge span {
1998 .pull-request-merge span {
1999 margin-right: 5px;
1999 margin-right: 5px;
2000 }
2000 }
2001
2001
2002 .pull-request-merge-actions {
2002 .pull-request-merge-actions {
2003 min-height: 30px;
2003 min-height: 30px;
2004 padding: 0px 0px;
2004 padding: 0px 0px;
2005 }
2005 }
2006
2006
2007 .pull-request-merge-info {
2007 .pull-request-merge-info {
2008 padding: 0px 5px 5px 0px;
2008 padding: 0px 5px 5px 0px;
2009 }
2009 }
2010
2010
2011 .merge-status {
2011 .merge-status {
2012 margin-right: 5px;
2012 margin-right: 5px;
2013 }
2013 }
2014
2014
2015 .merge-message {
2015 .merge-message {
2016 font-size: 1.2em
2016 font-size: 1.2em
2017 }
2017 }
2018
2018
2019 .merge-message.success i,
2019 .merge-message.success i,
2020 .merge-icon.success i {
2020 .merge-icon.success i {
2021 color:@alert1;
2021 color:@alert1;
2022 }
2022 }
2023
2023
2024 .merge-message.warning i,
2024 .merge-message.warning i,
2025 .merge-icon.warning i {
2025 .merge-icon.warning i {
2026 color: @alert3;
2026 color: @alert3;
2027 }
2027 }
2028
2028
2029 .merge-message.error i,
2029 .merge-message.error i,
2030 .merge-icon.error i {
2030 .merge-icon.error i {
2031 color:@alert2;
2031 color:@alert2;
2032 }
2032 }
2033
2033
2034 .pr-versions {
2034 .pr-versions {
2035 font-size: 1.1em;
2035 font-size: 1.1em;
2036 padding: 7.5px;
2036 padding: 7.5px;
2037
2037
2038 table {
2038 table {
2039
2039
2040 }
2040 }
2041
2041
2042 td {
2042 td {
2043 line-height: 15px;
2043 line-height: 15px;
2044 }
2044 }
2045
2045
2046 .compare-radio-button {
2046 .compare-radio-button {
2047 position: relative;
2047 position: relative;
2048 top: -3px;
2048 top: -3px;
2049 }
2049 }
2050 }
2050 }
2051
2051
2052
2052
2053 #close_pull_request {
2053 #close_pull_request {
2054 margin-right: 0px;
2054 margin-right: 0px;
2055 }
2055 }
2056
2056
2057 .empty_data {
2057 .empty_data {
2058 color: @grey4;
2058 color: @grey4;
2059 }
2059 }
2060
2060
2061 #changeset_compare_view_content {
2061 #changeset_compare_view_content {
2062 clear: both;
2062 clear: both;
2063 width: 100%;
2063 width: 100%;
2064 box-sizing: border-box;
2064 box-sizing: border-box;
2065 .border-radius(@border-radius);
2065 .border-radius(@border-radius);
2066
2066
2067 .help-block {
2067 .help-block {
2068 margin: @padding 0;
2068 margin: @padding 0;
2069 color: @text-color;
2069 color: @text-color;
2070 &.pre-formatting {
2070 &.pre-formatting {
2071 white-space: pre;
2071 white-space: pre;
2072 }
2072 }
2073 }
2073 }
2074
2074
2075 .empty_data {
2075 .empty_data {
2076 margin: @padding 0;
2076 margin: @padding 0;
2077 }
2077 }
2078
2078
2079 .alert {
2079 .alert {
2080 margin-bottom: @space;
2080 margin-bottom: @space;
2081 }
2081 }
2082 }
2082 }
2083
2083
2084 .table_disp {
2084 .table_disp {
2085 .status {
2085 .status {
2086 width: auto;
2086 width: auto;
2087 }
2087 }
2088 }
2088 }
2089
2089
2090
2090
2091 .creation_in_progress {
2091 .creation_in_progress {
2092 color: @grey4
2092 color: @grey4
2093 }
2093 }
2094
2094
2095 .status_box_menu {
2095 .status_box_menu {
2096 margin: 0;
2096 margin: 0;
2097 }
2097 }
2098
2098
2099 .notification-table{
2099 .notification-table{
2100 margin-bottom: @space;
2100 margin-bottom: @space;
2101 display: table;
2101 display: table;
2102 width: 100%;
2102 width: 100%;
2103
2103
2104 .container{
2104 .container{
2105 display: table-row;
2105 display: table-row;
2106
2106
2107 .notification-header{
2107 .notification-header{
2108 border-bottom: @border-thickness solid @border-default-color;
2108 border-bottom: @border-thickness solid @border-default-color;
2109 }
2109 }
2110
2110
2111 .notification-subject{
2111 .notification-subject{
2112 display: table-cell;
2112 display: table-cell;
2113 }
2113 }
2114 }
2114 }
2115 }
2115 }
2116
2116
2117 // Notifications
2117 // Notifications
2118 .notification-header{
2118 .notification-header{
2119 display: table;
2119 display: table;
2120 width: 100%;
2120 width: 100%;
2121 padding: floor(@basefontsize/2) 0;
2121 padding: floor(@basefontsize/2) 0;
2122 line-height: 1em;
2122 line-height: 1em;
2123
2123
2124 .desc, .delete-notifications, .read-notifications{
2124 .desc, .delete-notifications, .read-notifications{
2125 display: table-cell;
2125 display: table-cell;
2126 text-align: left;
2126 text-align: left;
2127 }
2127 }
2128
2128
2129 .delete-notifications, .read-notifications{
2129 .delete-notifications, .read-notifications{
2130 width: 35px;
2130 width: 35px;
2131 min-width: 35px; //fixes when only one button is displayed
2131 min-width: 35px; //fixes when only one button is displayed
2132 }
2132 }
2133 }
2133 }
2134
2134
2135 .notification-body {
2135 .notification-body {
2136 .markdown-block,
2136 .markdown-block,
2137 .rst-block {
2137 .rst-block {
2138 padding: @padding 0;
2138 padding: @padding 0;
2139 }
2139 }
2140
2140
2141 .notification-subject {
2141 .notification-subject {
2142 padding: @textmargin 0;
2142 padding: @textmargin 0;
2143 border-bottom: @border-thickness solid @border-default-color;
2143 border-bottom: @border-thickness solid @border-default-color;
2144 }
2144 }
2145 }
2145 }
2146
2146
2147 .notice-messages {
2147 .notice-messages {
2148 .markdown-block,
2148 .markdown-block,
2149 .rst-block {
2149 .rst-block {
2150 padding: 0;
2150 padding: 0;
2151 }
2151 }
2152 }
2152 }
2153
2153
2154 .notifications_buttons{
2154 .notifications_buttons{
2155 float: right;
2155 float: right;
2156 }
2156 }
2157
2157
2158 #notification-status{
2158 #notification-status{
2159 display: inline;
2159 display: inline;
2160 }
2160 }
2161
2161
2162 // Repositories
2162 // Repositories
2163
2163
2164 #summary.fields{
2164 #summary.fields{
2165 display: table;
2165 display: table;
2166
2166
2167 .field{
2167 .field{
2168 display: table-row;
2168 display: table-row;
2169
2169
2170 .label-summary{
2170 .label-summary{
2171 display: table-cell;
2171 display: table-cell;
2172 min-width: @label-summary-minwidth;
2172 min-width: @label-summary-minwidth;
2173 padding-top: @padding/2;
2173 padding-top: @padding/2;
2174 padding-bottom: @padding/2;
2174 padding-bottom: @padding/2;
2175 padding-right: @padding/2;
2175 padding-right: @padding/2;
2176 }
2176 }
2177
2177
2178 .input{
2178 .input{
2179 display: table-cell;
2179 display: table-cell;
2180 padding: @padding/2;
2180 padding: @padding/2;
2181
2181
2182 input{
2182 input{
2183 min-width: 29em;
2183 min-width: 29em;
2184 padding: @padding/4;
2184 padding: @padding/4;
2185 }
2185 }
2186 }
2186 }
2187 .statistics, .downloads{
2187 .statistics, .downloads{
2188 .disabled{
2188 .disabled{
2189 color: @grey4;
2189 color: @grey4;
2190 }
2190 }
2191 }
2191 }
2192 }
2192 }
2193 }
2193 }
2194
2194
2195 #summary{
2195 #summary{
2196 width: 70%;
2196 width: 70%;
2197 }
2197 }
2198
2198
2199
2199
2200 // Journal
2200 // Journal
2201 .journal.title {
2201 .journal.title {
2202 h5 {
2202 h5 {
2203 float: left;
2203 float: left;
2204 margin: 0;
2204 margin: 0;
2205 width: 70%;
2205 width: 70%;
2206 }
2206 }
2207
2207
2208 ul {
2208 ul {
2209 float: right;
2209 float: right;
2210 display: inline-block;
2210 display: inline-block;
2211 margin: 0;
2211 margin: 0;
2212 width: 30%;
2212 width: 30%;
2213 text-align: right;
2213 text-align: right;
2214
2214
2215 li {
2215 li {
2216 display: inline;
2216 display: inline;
2217 font-size: @journal-fontsize;
2217 font-size: @journal-fontsize;
2218 line-height: 1em;
2218 line-height: 1em;
2219
2219
2220 list-style-type: none;
2220 list-style-type: none;
2221 }
2221 }
2222 }
2222 }
2223 }
2223 }
2224
2224
2225 .filterexample {
2225 .filterexample {
2226 position: absolute;
2226 position: absolute;
2227 top: 95px;
2227 top: 95px;
2228 left: @contentpadding;
2228 left: @contentpadding;
2229 color: @rcblue;
2229 color: @rcblue;
2230 font-size: 11px;
2230 font-size: 11px;
2231 font-family: @text-regular;
2231 font-family: @text-regular;
2232 cursor: help;
2232 cursor: help;
2233
2233
2234 &:hover {
2234 &:hover {
2235 color: @rcdarkblue;
2235 color: @rcdarkblue;
2236 }
2236 }
2237
2237
2238 @media (max-width:768px) {
2238 @media (max-width:768px) {
2239 position: relative;
2239 position: relative;
2240 top: auto;
2240 top: auto;
2241 left: auto;
2241 left: auto;
2242 display: block;
2242 display: block;
2243 }
2243 }
2244 }
2244 }
2245
2245
2246
2246
2247 #journal{
2247 #journal{
2248 margin-bottom: @space;
2248 margin-bottom: @space;
2249
2249
2250 .journal_day{
2250 .journal_day{
2251 margin-bottom: @textmargin/2;
2251 margin-bottom: @textmargin/2;
2252 padding-bottom: @textmargin/2;
2252 padding-bottom: @textmargin/2;
2253 font-size: @journal-fontsize;
2253 font-size: @journal-fontsize;
2254 border-bottom: @border-thickness solid @border-default-color;
2254 border-bottom: @border-thickness solid @border-default-color;
2255 }
2255 }
2256
2256
2257 .journal_container{
2257 .journal_container{
2258 margin-bottom: @space;
2258 margin-bottom: @space;
2259
2259
2260 .journal_user{
2260 .journal_user{
2261 display: inline-block;
2261 display: inline-block;
2262 }
2262 }
2263 .journal_action_container{
2263 .journal_action_container{
2264 display: block;
2264 display: block;
2265 margin-top: @textmargin;
2265 margin-top: @textmargin;
2266
2266
2267 div{
2267 div{
2268 display: inline;
2268 display: inline;
2269 }
2269 }
2270
2270
2271 div.journal_action_params{
2271 div.journal_action_params{
2272 display: block;
2272 display: block;
2273 }
2273 }
2274
2274
2275 div.journal_repo:after{
2275 div.journal_repo:after{
2276 content: "\A";
2276 content: "\A";
2277 white-space: pre;
2277 white-space: pre;
2278 }
2278 }
2279
2279
2280 div.date{
2280 div.date{
2281 display: block;
2281 display: block;
2282 margin-bottom: @textmargin;
2282 margin-bottom: @textmargin;
2283 }
2283 }
2284 }
2284 }
2285 }
2285 }
2286 }
2286 }
2287
2287
2288 // Files
2288 // Files
2289 .edit-file-title {
2289 .edit-file-title {
2290 font-size: 16px;
2290 font-size: 16px;
2291
2291
2292 .title-heading {
2292 .title-heading {
2293 padding: 2px;
2293 padding: 2px;
2294 }
2294 }
2295 }
2295 }
2296
2296
2297 .edit-file-fieldset {
2297 .edit-file-fieldset {
2298 margin: @sidebarpadding 0;
2298 margin: @sidebarpadding 0;
2299
2299
2300 .fieldset {
2300 .fieldset {
2301 .left-label {
2301 .left-label {
2302 width: 13%;
2302 width: 13%;
2303 }
2303 }
2304 .right-content {
2304 .right-content {
2305 width: 87%;
2305 width: 87%;
2306 max-width: 100%;
2306 max-width: 100%;
2307 }
2307 }
2308 .filename-label {
2308 .filename-label {
2309 margin-top: 13px;
2309 margin-top: 13px;
2310 }
2310 }
2311 .commit-message-label {
2311 .commit-message-label {
2312 margin-top: 4px;
2312 margin-top: 4px;
2313 }
2313 }
2314 .file-upload-input {
2314 .file-upload-input {
2315 input {
2315 input {
2316 display: none;
2316 display: none;
2317 }
2317 }
2318 margin-top: 10px;
2318 margin-top: 10px;
2319 }
2319 }
2320 .file-upload-label {
2320 .file-upload-label {
2321 margin-top: 10px;
2321 margin-top: 10px;
2322 }
2322 }
2323 p {
2323 p {
2324 margin-top: 5px;
2324 margin-top: 5px;
2325 }
2325 }
2326
2326
2327 }
2327 }
2328 .custom-path-link {
2328 .custom-path-link {
2329 margin-left: 5px;
2329 margin-left: 5px;
2330 }
2330 }
2331 #commit {
2331 #commit {
2332 resize: vertical;
2332 resize: vertical;
2333 }
2333 }
2334 }
2334 }
2335
2335
2336 .delete-file-preview {
2336 .delete-file-preview {
2337 max-height: 250px;
2337 max-height: 250px;
2338 }
2338 }
2339
2339
2340 .new-file,
2340 .new-file,
2341 #filter_activate,
2341 #filter_activate,
2342 #filter_deactivate {
2342 #filter_deactivate {
2343 float: right;
2343 float: right;
2344 margin: 0 0 0 10px;
2344 margin: 0 0 0 10px;
2345 }
2345 }
2346
2346
2347 .file-upload-transaction-wrapper {
2347 .file-upload-transaction-wrapper {
2348 margin-top: 57px;
2348 margin-top: 57px;
2349 clear: both;
2349 clear: both;
2350 }
2350 }
2351
2351
2352 .file-upload-transaction-wrapper .error {
2352 .file-upload-transaction-wrapper .error {
2353 color: @color5;
2353 color: @color5;
2354 }
2354 }
2355
2355
2356 .file-upload-transaction {
2356 .file-upload-transaction {
2357 min-height: 200px;
2357 min-height: 200px;
2358 padding: 54px;
2358 padding: 54px;
2359 border: 1px solid @grey5;
2359 border: 1px solid @grey5;
2360 text-align: center;
2360 text-align: center;
2361 clear: both;
2361 clear: both;
2362 }
2362 }
2363
2363
2364 .file-upload-transaction i {
2364 .file-upload-transaction i {
2365 font-size: 48px
2365 font-size: 48px
2366 }
2366 }
2367
2367
2368 h3.files_location{
2368 h3.files_location{
2369 line-height: 2.4em;
2369 line-height: 2.4em;
2370 }
2370 }
2371
2371
2372 .browser-nav {
2372 .browser-nav {
2373 width: 100%;
2373 width: 100%;
2374 display: table;
2374 display: table;
2375 margin-bottom: 20px;
2375 margin-bottom: 20px;
2376
2376
2377 .info_box {
2377 .info_box {
2378 float: left;
2378 float: left;
2379 display: inline-table;
2379 display: inline-table;
2380 height: 2.5em;
2380 height: 2.5em;
2381
2381
2382 .browser-cur-rev, .info_box_elem {
2382 .browser-cur-rev, .info_box_elem {
2383 display: table-cell;
2383 display: table-cell;
2384 vertical-align: middle;
2384 vertical-align: middle;
2385 }
2385 }
2386
2386
2387 .drop-menu {
2387 .drop-menu {
2388 margin: 0 10px;
2388 margin: 0 10px;
2389 }
2389 }
2390
2390
2391 .info_box_elem {
2391 .info_box_elem {
2392 border-top: @border-thickness solid @grey5;
2392 border-top: @border-thickness solid @grey5;
2393 border-bottom: @border-thickness solid @grey5;
2393 border-bottom: @border-thickness solid @grey5;
2394 box-shadow: @button-shadow;
2394 box-shadow: @button-shadow;
2395
2395
2396 #at_rev, a {
2396 #at_rev, a {
2397 padding: 0.6em 0.4em;
2397 padding: 0.6em 0.4em;
2398 margin: 0;
2398 margin: 0;
2399 .box-shadow(none);
2399 .box-shadow(none);
2400 border: 0;
2400 border: 0;
2401 height: 12px;
2401 height: 12px;
2402 color: @grey2;
2402 color: @grey2;
2403 }
2403 }
2404
2404
2405 input#at_rev {
2405 input#at_rev {
2406 max-width: 50px;
2406 max-width: 50px;
2407 text-align: center;
2407 text-align: center;
2408 }
2408 }
2409
2409
2410 &.previous {
2410 &.previous {
2411 border: @border-thickness solid @grey5;
2411 border: @border-thickness solid @grey5;
2412 border-top-left-radius: @border-radius;
2412 border-top-left-radius: @border-radius;
2413 border-bottom-left-radius: @border-radius;
2413 border-bottom-left-radius: @border-radius;
2414
2414
2415 &:hover {
2415 &:hover {
2416 border-color: @grey4;
2416 border-color: @grey4;
2417 }
2417 }
2418
2418
2419 .disabled {
2419 .disabled {
2420 color: @grey5;
2420 color: @grey5;
2421 cursor: not-allowed;
2421 cursor: not-allowed;
2422 opacity: 0.5;
2422 opacity: 0.5;
2423 }
2423 }
2424 }
2424 }
2425
2425
2426 &.next {
2426 &.next {
2427 border: @border-thickness solid @grey5;
2427 border: @border-thickness solid @grey5;
2428 border-top-right-radius: @border-radius;
2428 border-top-right-radius: @border-radius;
2429 border-bottom-right-radius: @border-radius;
2429 border-bottom-right-radius: @border-radius;
2430
2430
2431 &:hover {
2431 &:hover {
2432 border-color: @grey4;
2432 border-color: @grey4;
2433 }
2433 }
2434
2434
2435 .disabled {
2435 .disabled {
2436 color: @grey5;
2436 color: @grey5;
2437 cursor: not-allowed;
2437 cursor: not-allowed;
2438 opacity: 0.5;
2438 opacity: 0.5;
2439 }
2439 }
2440 }
2440 }
2441 }
2441 }
2442
2442
2443 .browser-cur-rev {
2443 .browser-cur-rev {
2444
2444
2445 span{
2445 span{
2446 margin: 0;
2446 margin: 0;
2447 color: @rcblue;
2447 color: @rcblue;
2448 height: 12px;
2448 height: 12px;
2449 display: inline-block;
2449 display: inline-block;
2450 padding: 0.7em 1em ;
2450 padding: 0.7em 1em ;
2451 border: @border-thickness solid @rcblue;
2451 border: @border-thickness solid @rcblue;
2452 margin-right: @padding;
2452 margin-right: @padding;
2453 }
2453 }
2454 }
2454 }
2455
2455
2456 }
2456 }
2457
2457
2458 .select-index-number {
2458 .select-index-number {
2459 margin: 0 0 0 20px;
2459 margin: 0 0 0 20px;
2460 color: @grey3;
2460 color: @grey3;
2461 }
2461 }
2462
2462
2463 .search_activate {
2463 .search_activate {
2464 display: table-cell;
2464 display: table-cell;
2465 vertical-align: middle;
2465 vertical-align: middle;
2466
2466
2467 input, label{
2467 input, label{
2468 margin: 0;
2468 margin: 0;
2469 padding: 0;
2469 padding: 0;
2470 }
2470 }
2471
2471
2472 input{
2472 input{
2473 margin-left: @textmargin;
2473 margin-left: @textmargin;
2474 }
2474 }
2475
2475
2476 }
2476 }
2477 }
2477 }
2478
2478
2479 .browser-cur-rev{
2479 .browser-cur-rev{
2480 margin-bottom: @textmargin;
2480 margin-bottom: @textmargin;
2481 }
2481 }
2482
2482
2483 #node_filter_box_loading{
2483 #node_filter_box_loading{
2484 .info_text;
2484 .info_text;
2485 }
2485 }
2486
2486
2487 .browser-search {
2487 .browser-search {
2488 margin: -25px 0px 5px 0px;
2488 margin: -25px 0px 5px 0px;
2489 }
2489 }
2490
2490
2491 .files-quick-filter {
2491 .files-quick-filter {
2492 float: right;
2492 float: right;
2493 width: 180px;
2493 width: 180px;
2494 position: relative;
2494 position: relative;
2495 }
2495 }
2496
2496
2497 .files-filter-box {
2497 .files-filter-box {
2498 display: flex;
2498 display: flex;
2499 padding: 0px;
2499 padding: 0px;
2500 border-radius: 3px;
2500 border-radius: 3px;
2501 margin-bottom: 0;
2501 margin-bottom: 0;
2502
2502
2503 a {
2503 a {
2504 border: none !important;
2504 border: none !important;
2505 }
2505 }
2506
2506
2507 li {
2507 li {
2508 list-style-type: none
2508 list-style-type: none
2509 }
2509 }
2510 }
2510 }
2511
2511
2512 .files-filter-box-path {
2512 .files-filter-box-path {
2513 line-height: 33px;
2513 line-height: 33px;
2514 padding: 0;
2514 padding: 0;
2515 width: 20px;
2515 width: 20px;
2516 position: absolute;
2516 position: absolute;
2517 z-index: 11;
2517 z-index: 11;
2518 left: 5px;
2518 left: 5px;
2519 }
2519 }
2520
2520
2521 .files-filter-box-input {
2521 .files-filter-box-input {
2522 margin-right: 0;
2522 margin-right: 0;
2523
2523
2524 input {
2524 input {
2525 border: 1px solid @white;
2525 border: 1px solid @white;
2526 padding-left: 25px;
2526 padding-left: 25px;
2527 width: 145px;
2527 width: 145px;
2528
2528
2529 &:hover {
2529 &:hover {
2530 border-color: @grey6;
2530 border-color: @grey6;
2531 }
2531 }
2532
2532
2533 &:focus {
2533 &:focus {
2534 border-color: @grey5;
2534 border-color: @grey5;
2535 }
2535 }
2536 }
2536 }
2537 }
2537 }
2538
2538
2539 .browser-result{
2539 .browser-result{
2540 td a{
2540 td a{
2541 margin-left: 0.5em;
2541 margin-left: 0.5em;
2542 display: inline-block;
2542 display: inline-block;
2543
2543
2544 em {
2544 em {
2545 font-weight: @text-bold-weight;
2545 font-weight: @text-bold-weight;
2546 font-family: @text-bold;
2546 font-family: @text-bold;
2547 }
2547 }
2548 }
2548 }
2549 }
2549 }
2550
2550
2551 .browser-highlight{
2551 .browser-highlight{
2552 background-color: @grey5-alpha;
2552 background-color: @grey5-alpha;
2553 }
2553 }
2554
2554
2555
2555
2556 .edit-file-fieldset #location,
2556 .edit-file-fieldset #location,
2557 .edit-file-fieldset #filename {
2557 .edit-file-fieldset #filename {
2558 display: flex;
2558 display: flex;
2559 width: -moz-available; /* WebKit-based browsers will ignore this. */
2559 width: -moz-available; /* WebKit-based browsers will ignore this. */
2560 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2560 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2561 width: fill-available;
2561 width: fill-available;
2562 border: 0;
2562 border: 0;
2563 }
2563 }
2564
2564
2565 .path-items {
2565 .path-items {
2566 display: flex;
2566 display: flex;
2567 padding: 0;
2567 padding: 0;
2568 border: 1px solid #eeeeee;
2568 border: 1px solid #eeeeee;
2569 width: 100%;
2569 width: 100%;
2570 float: left;
2570 float: left;
2571
2571
2572 .breadcrumb-path {
2572 .breadcrumb-path {
2573 line-height: 30px;
2573 line-height: 30px;
2574 padding: 0 4px;
2574 padding: 0 4px;
2575 white-space: nowrap;
2575 white-space: nowrap;
2576 }
2576 }
2577
2577
2578 .upload-form {
2578 .upload-form {
2579 margin-top: 46px;
2579 margin-top: 46px;
2580 }
2580 }
2581
2581
2582 .location-path {
2582 .location-path {
2583 width: -moz-available; /* WebKit-based browsers will ignore this. */
2583 width: -moz-available; /* WebKit-based browsers will ignore this. */
2584 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2584 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2585 width: fill-available;
2585 width: fill-available;
2586
2586
2587 .file-name-input {
2587 .file-name-input {
2588 padding: 0.5em 0;
2588 padding: 0.5em 0;
2589 }
2589 }
2590
2590
2591 }
2591 }
2592
2592
2593 ul {
2593 ul {
2594 display: flex;
2594 display: flex;
2595 margin: 0;
2595 margin: 0;
2596 padding: 0;
2596 padding: 0;
2597 width: 100%;
2597 width: 100%;
2598 }
2598 }
2599
2599
2600 li {
2600 li {
2601 list-style-type: none;
2601 list-style-type: none;
2602 }
2602 }
2603
2603
2604 }
2604 }
2605
2605
2606 .editor-items {
2606 .editor-items {
2607 height: 40px;
2607 height: 40px;
2608 margin: 10px 0 -17px 10px;
2608 margin: 10px 0 -17px 10px;
2609
2609
2610 .editor-action {
2610 .editor-action {
2611 cursor: pointer;
2611 cursor: pointer;
2612 }
2612 }
2613
2613
2614 .editor-action.active {
2614 .editor-action.active {
2615 border-bottom: 2px solid #5C5C5C;
2615 border-bottom: 2px solid #5C5C5C;
2616 }
2616 }
2617
2617
2618 li {
2618 li {
2619 list-style-type: none;
2619 list-style-type: none;
2620 }
2620 }
2621 }
2621 }
2622
2622
2623 .edit-file-fieldset .message textarea {
2623 .edit-file-fieldset .message textarea {
2624 border: 1px solid #eeeeee;
2624 border: 1px solid #eeeeee;
2625 }
2625 }
2626
2626
2627 #files_data .codeblock {
2627 #files_data .codeblock {
2628 background-color: #F5F5F5;
2628 background-color: #F5F5F5;
2629 }
2629 }
2630
2630
2631 #editor_preview {
2631 #editor_preview {
2632 background: white;
2632 background: white;
2633 }
2633 }
2634
2634
2635 .show-editor {
2635 .show-editor {
2636 padding: 10px;
2636 padding: 10px;
2637 background-color: white;
2637 background-color: white;
2638
2638
2639 }
2639 }
2640
2640
2641 .show-preview {
2641 .show-preview {
2642 padding: 10px;
2642 padding: 10px;
2643 background-color: white;
2643 background-color: white;
2644 border-left: 1px solid #eeeeee;
2644 border-left: 1px solid #eeeeee;
2645 }
2645 }
2646 // quick filter
2646 // quick filter
2647 .grid-quick-filter {
2647 .grid-quick-filter {
2648 float: right;
2648 float: right;
2649 position: relative;
2649 position: relative;
2650 }
2650 }
2651
2651
2652 .grid-filter-box {
2652 .grid-filter-box {
2653 display: flex;
2653 display: flex;
2654 padding: 0px;
2654 padding: 0px;
2655 border-radius: 3px;
2655 border-radius: 3px;
2656 margin-bottom: 0;
2656 margin-bottom: 0;
2657
2657
2658 a {
2658 a {
2659 border: none !important;
2659 border: none !important;
2660 }
2660 }
2661
2661
2662 li {
2662 li {
2663 list-style-type: none
2663 list-style-type: none
2664 }
2664 }
2665
2665
2666 }
2666 }
2667
2667
2668 .grid-filter-box-icon {
2668 .grid-filter-box-icon {
2669 line-height: 33px;
2669 line-height: 33px;
2670 padding: 0;
2670 padding: 0;
2671 width: 20px;
2671 width: 20px;
2672 position: absolute;
2672 position: absolute;
2673 z-index: 11;
2673 z-index: 11;
2674 left: 5px;
2674 left: 5px;
2675 }
2675 }
2676
2676
2677 .grid-filter-box-input {
2677 .grid-filter-box-input {
2678 margin-right: 0;
2678 margin-right: 0;
2679
2679
2680 input {
2680 input {
2681 border: 1px solid @white;
2681 border: 1px solid @white;
2682 padding-left: 25px;
2682 padding-left: 25px;
2683 width: 145px;
2683 width: 145px;
2684
2684
2685 &:hover {
2685 &:hover {
2686 border-color: @grey6;
2686 border-color: @grey6;
2687 }
2687 }
2688
2688
2689 &:focus {
2689 &:focus {
2690 border-color: @grey5;
2690 border-color: @grey5;
2691 }
2691 }
2692 }
2692 }
2693 }
2693 }
2694
2694
2695
2695
2696
2696
2697 // Search
2697 // Search
2698
2698
2699 .search-form{
2699 .search-form{
2700 #q {
2700 #q {
2701 width: @search-form-width;
2701 width: @search-form-width;
2702 }
2702 }
2703 .fields{
2703 .fields{
2704 margin: 0 0 @space;
2704 margin: 0 0 @space;
2705 }
2705 }
2706
2706
2707 label{
2707 label{
2708 display: inline-block;
2708 display: inline-block;
2709 margin-right: @textmargin;
2709 margin-right: @textmargin;
2710 padding-top: 0.25em;
2710 padding-top: 0.25em;
2711 }
2711 }
2712
2712
2713
2713
2714 .results{
2714 .results{
2715 clear: both;
2715 clear: both;
2716 margin: 0 0 @padding;
2716 margin: 0 0 @padding;
2717 }
2717 }
2718
2718
2719 .search-tags {
2719 .search-tags {
2720 padding: 5px 0;
2720 padding: 5px 0;
2721 }
2721 }
2722 }
2722 }
2723
2723
2724 div.search-feedback-items {
2724 div.search-feedback-items {
2725 display: inline-block;
2725 display: inline-block;
2726 }
2726 }
2727
2727
2728 div.search-code-body {
2728 div.search-code-body {
2729 background-color: #ffffff; padding: 5px 0 5px 10px;
2729 background-color: #ffffff; padding: 5px 0 5px 10px;
2730 pre {
2730 pre {
2731 .match { background-color: #faffa6;}
2731 .match { background-color: #faffa6;}
2732 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2732 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2733 }
2733 }
2734 }
2734 }
2735
2735
2736 .expand_commit.search {
2736 .expand_commit.search {
2737 .show_more.open {
2737 .show_more.open {
2738 height: auto;
2738 height: auto;
2739 max-height: none;
2739 max-height: none;
2740 }
2740 }
2741 }
2741 }
2742
2742
2743 .search-results {
2743 .search-results {
2744
2744
2745 h2 {
2745 h2 {
2746 margin-bottom: 0;
2746 margin-bottom: 0;
2747 }
2747 }
2748 .codeblock {
2748 .codeblock {
2749 border: none;
2749 border: none;
2750 background: transparent;
2750 background: transparent;
2751 }
2751 }
2752
2752
2753 .codeblock-header {
2753 .codeblock-header {
2754 border: none;
2754 border: none;
2755 background: transparent;
2755 background: transparent;
2756 }
2756 }
2757
2757
2758 .code-body {
2758 .code-body {
2759 border: @border-thickness solid @grey6;
2759 border: @border-thickness solid @grey6;
2760 .border-radius(@border-radius);
2760 .border-radius(@border-radius);
2761 }
2761 }
2762
2762
2763 .td-commit {
2763 .td-commit {
2764 &:extend(pre);
2764 &:extend(pre);
2765 border-bottom: @border-thickness solid @border-default-color;
2765 border-bottom: @border-thickness solid @border-default-color;
2766 }
2766 }
2767
2767
2768 .message {
2768 .message {
2769 height: auto;
2769 height: auto;
2770 max-width: 350px;
2770 max-width: 350px;
2771 white-space: normal;
2771 white-space: normal;
2772 text-overflow: initial;
2772 text-overflow: initial;
2773 overflow: visible;
2773 overflow: visible;
2774
2774
2775 .match { background-color: #faffa6;}
2775 .match { background-color: #faffa6;}
2776 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2776 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2777 }
2777 }
2778
2778
2779 .path {
2779 .path {
2780 border-bottom: none !important;
2780 border-bottom: none !important;
2781 border-left: 1px solid @grey6 !important;
2781 border-left: 1px solid @grey6 !important;
2782 border-right: 1px solid @grey6 !important;
2782 border-right: 1px solid @grey6 !important;
2783 }
2783 }
2784 }
2784 }
2785
2785
2786 table.rctable td.td-search-results div {
2786 table.rctable td.td-search-results div {
2787 max-width: 100%;
2787 max-width: 100%;
2788 }
2788 }
2789
2789
2790 #tip-box, .tip-box{
2790 #tip-box, .tip-box{
2791 padding: @menupadding/2;
2791 padding: @menupadding/2;
2792 display: block;
2792 display: block;
2793 border: @border-thickness solid @border-highlight-color;
2793 border: @border-thickness solid @border-highlight-color;
2794 .border-radius(@border-radius);
2794 .border-radius(@border-radius);
2795 background-color: white;
2795 background-color: white;
2796 z-index: 99;
2796 z-index: 99;
2797 white-space: pre-wrap;
2797 white-space: pre-wrap;
2798 }
2798 }
2799
2799
2800 #linktt {
2800 #linktt {
2801 width: 79px;
2801 width: 79px;
2802 }
2802 }
2803
2803
2804 #help_kb .modal-content{
2804 #help_kb .modal-content{
2805 max-width: 800px;
2805 max-width: 800px;
2806 margin: 10% auto;
2806 margin: 10% auto;
2807
2807
2808 table{
2808 table{
2809 td,th{
2809 td,th{
2810 border-bottom: none;
2810 border-bottom: none;
2811 line-height: 2.5em;
2811 line-height: 2.5em;
2812 }
2812 }
2813 th{
2813 th{
2814 padding-bottom: @textmargin/2;
2814 padding-bottom: @textmargin/2;
2815 }
2815 }
2816 td.keys{
2816 td.keys{
2817 text-align: center;
2817 text-align: center;
2818 }
2818 }
2819 }
2819 }
2820
2820
2821 .block-left{
2821 .block-left{
2822 width: 45%;
2822 width: 45%;
2823 margin-right: 5%;
2823 margin-right: 5%;
2824 }
2824 }
2825 .modal-footer{
2825 .modal-footer{
2826 clear: both;
2826 clear: both;
2827 }
2827 }
2828 .key.tag{
2828 .key.tag{
2829 padding: 0.5em;
2829 padding: 0.5em;
2830 background-color: @rcblue;
2830 background-color: @rcblue;
2831 color: white;
2831 color: white;
2832 border-color: @rcblue;
2832 border-color: @rcblue;
2833 .box-shadow(none);
2833 .box-shadow(none);
2834 }
2834 }
2835 }
2835 }
2836
2836
2837
2837
2838
2838
2839 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2839 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2840
2840
2841 @import 'statistics-graph';
2841 @import 'statistics-graph';
2842 @import 'tables';
2842 @import 'tables';
2843 @import 'forms';
2843 @import 'forms';
2844 @import 'diff';
2844 @import 'diff';
2845 @import 'summary';
2845 @import 'summary';
2846 @import 'navigation';
2846 @import 'navigation';
2847
2847
2848 //--- SHOW/HIDE SECTIONS --//
2848 //--- SHOW/HIDE SECTIONS --//
2849
2849
2850 .btn-collapse {
2850 .btn-collapse {
2851 float: right;
2851 float: right;
2852 text-align: right;
2852 text-align: right;
2853 font-family: @text-light;
2853 font-family: @text-light;
2854 font-size: @basefontsize;
2854 font-size: @basefontsize;
2855 cursor: pointer;
2855 cursor: pointer;
2856 border: none;
2856 border: none;
2857 color: @rcblue;
2857 color: @rcblue;
2858 }
2858 }
2859
2859
2860 table.rctable,
2860 table.rctable,
2861 table.dataTable {
2861 table.dataTable {
2862 .btn-collapse {
2862 .btn-collapse {
2863 float: right;
2863 float: right;
2864 text-align: right;
2864 text-align: right;
2865 }
2865 }
2866 }
2866 }
2867
2867
2868 table.rctable {
2868 table.rctable {
2869 &.permissions {
2869 &.permissions {
2870
2870
2871 th.td-owner {
2871 th.td-owner {
2872 padding: 0;
2872 padding: 0;
2873 }
2873 }
2874
2874
2875 th {
2875 th {
2876 font-weight: normal;
2876 font-weight: normal;
2877 padding: 0 5px;
2877 padding: 0 5px;
2878 }
2878 }
2879
2879
2880 }
2880 }
2881 }
2881 }
2882
2882
2883
2883
2884 // TODO: johbo: Fix for IE10, this avoids that we see a border
2884 // TODO: johbo: Fix for IE10, this avoids that we see a border
2885 // and padding around checkboxes and radio boxes. Move to the right place,
2885 // and padding around checkboxes and radio boxes. Move to the right place,
2886 // or better: Remove this once we did the form refactoring.
2886 // or better: Remove this once we did the form refactoring.
2887 input[type=checkbox],
2887 input[type=checkbox],
2888 input[type=radio] {
2888 input[type=radio] {
2889 padding: 0;
2889 padding: 0;
2890 border: none;
2890 border: none;
2891 }
2891 }
2892
2892
2893 .toggle-ajax-spinner{
2893 .toggle-ajax-spinner{
2894 height: 16px;
2894 height: 16px;
2895 width: 16px;
2895 width: 16px;
2896 }
2896 }
2897
2897
2898
2898
2899 .markup-form .clearfix {
2899 .markup-form .clearfix {
2900 .border-radius(@border-radius);
2900 .border-radius(@border-radius);
2901 margin: 0px;
2901 margin: 0px;
2902 }
2902 }
2903
2903
2904 .markup-form-area {
2904 .markup-form-area {
2905 padding: 8px 12px;
2905 padding: 8px 12px;
2906 border: 1px solid @grey4;
2906 border: 1px solid @grey4;
2907 .border-radius(@border-radius);
2907 .border-radius(@border-radius);
2908 }
2908 }
2909
2909
2910 .markup-form-area-header .nav-links {
2910 .markup-form-area-header .nav-links {
2911 display: flex;
2911 display: flex;
2912 flex-flow: row wrap;
2912 flex-flow: row wrap;
2913 -webkit-flex-flow: row wrap;
2913 -webkit-flex-flow: row wrap;
2914 width: 100%;
2914 width: 100%;
2915 }
2915 }
2916
2916
2917 .markup-form-area-footer {
2917 .markup-form-area-footer {
2918 display: flex;
2918 display: flex;
2919 }
2919 }
2920
2920
2921 .markup-form-area-footer .toolbar {
2921 .markup-form-area-footer .toolbar {
2922
2922
2923 }
2923 }
2924
2924
2925 // markup Form
2925 // markup Form
2926 div.markup-form {
2926 div.markup-form {
2927 margin-top: 20px;
2927 margin-top: 20px;
2928 }
2928 }
2929
2929
2930 .markup-form strong {
2930 .markup-form strong {
2931 display: block;
2931 display: block;
2932 margin-bottom: 15px;
2932 margin-bottom: 15px;
2933 }
2933 }
2934
2934
2935 .markup-form textarea {
2935 .markup-form textarea {
2936 width: 100%;
2936 width: 100%;
2937 height: 100px;
2937 height: 100px;
2938 font-family: @text-monospace;
2938 font-family: @text-monospace;
2939 }
2939 }
2940
2940
2941 form.markup-form {
2941 form.markup-form {
2942 margin-top: 10px;
2942 margin-top: 10px;
2943 margin-left: 10px;
2943 margin-left: 10px;
2944 }
2944 }
2945
2945
2946 .markup-form .comment-block-ta,
2946 .markup-form .comment-block-ta,
2947 .markup-form .preview-box {
2947 .markup-form .preview-box {
2948 .border-radius(@border-radius);
2948 .border-radius(@border-radius);
2949 .box-sizing(border-box);
2949 .box-sizing(border-box);
2950 background-color: white;
2950 background-color: white;
2951 }
2951 }
2952
2952
2953 .markup-form .preview-box.unloaded {
2953 .markup-form .preview-box.unloaded {
2954 height: 50px;
2954 height: 50px;
2955 text-align: center;
2955 text-align: center;
2956 padding: 20px;
2956 padding: 20px;
2957 background-color: white;
2957 background-color: white;
2958 }
2958 }
2959
2959
2960
2960
2961 .dropzone-wrapper {
2961 .dropzone-wrapper {
2962 border: 1px solid @grey5;
2962 border: 1px solid @grey5;
2963 padding: 20px;
2963 padding: 20px;
2964 }
2964 }
2965
2965
2966 .dropzone,
2966 .dropzone,
2967 .dropzone-pure {
2967 .dropzone-pure {
2968 border: 2px dashed @grey5;
2968 border: 2px dashed @grey5;
2969 border-radius: 5px;
2969 border-radius: 5px;
2970 background: white;
2970 background: white;
2971 min-height: 200px;
2971 min-height: 200px;
2972 padding: 54px;
2972 padding: 54px;
2973
2973
2974 .dz-message {
2974 .dz-message {
2975 font-weight: 700;
2975 font-weight: 700;
2976 text-align: center;
2976 text-align: center;
2977 margin: 2em 0;
2977 margin: 2em 0;
2978 }
2978 }
2979
2979
2980 }
2980 }
2981
2981
2982 .dz-preview {
2982 .dz-preview {
2983 margin: 10px 0 !important;
2983 margin: 10px 0 !important;
2984 position: relative;
2984 position: relative;
2985 vertical-align: top;
2985 vertical-align: top;
2986 padding: 10px;
2986 padding: 10px;
2987 border-bottom: 1px solid @grey5;
2987 border-bottom: 1px solid @grey5;
2988 }
2988 }
2989
2989
2990 .dz-filename {
2990 .dz-filename {
2991 font-weight: 700;
2991 font-weight: 700;
2992 float: left;
2992 float: left;
2993 }
2993 }
2994
2994
2995 .dz-sending {
2995 .dz-sending {
2996 float: right;
2996 float: right;
2997 }
2997 }
2998
2998
2999 .dz-response {
2999 .dz-response {
3000 clear: both
3000 clear: both
3001 }
3001 }
3002
3002
3003 .dz-filename-size {
3003 .dz-filename-size {
3004 float: right
3004 float: right
3005 }
3005 }
3006
3006
3007 .dz-error-message {
3007 .dz-error-message {
3008 color: @alert2;
3008 color: @alert2;
3009 padding-top: 10px;
3009 padding-top: 10px;
3010 clear: both;
3010 clear: both;
3011 }
3011 }
3012
3012
3013
3013
3014 .user-hovercard {
3014 .user-hovercard {
3015 padding: 5px;
3015 padding: 5px;
3016 }
3016 }
3017
3017
3018 .user-hovercard-icon {
3018 .user-hovercard-icon {
3019 display: inline;
3019 display: inline;
3020 padding: 0;
3020 padding: 0;
3021 box-sizing: content-box;
3021 box-sizing: content-box;
3022 border-radius: 50%;
3022 border-radius: 50%;
3023 float: left;
3023 float: left;
3024 }
3024 }
3025
3025
3026 .user-hovercard-name {
3026 .user-hovercard-name {
3027 float: right;
3027 float: right;
3028 vertical-align: top;
3028 vertical-align: top;
3029 padding-left: 10px;
3029 padding-left: 10px;
3030 min-width: 150px;
3030 min-width: 150px;
3031 }
3031 }
3032
3032
3033 .user-hovercard-bio {
3033 .user-hovercard-bio {
3034 clear: both;
3034 clear: both;
3035 padding-top: 10px;
3035 padding-top: 10px;
3036 }
3036 }
3037
3037
3038 .user-hovercard-header {
3038 .user-hovercard-header {
3039 clear: both;
3039 clear: both;
3040 min-height: 10px;
3040 min-height: 10px;
3041 }
3041 }
3042
3042
3043 .user-hovercard-footer {
3043 .user-hovercard-footer {
3044 clear: both;
3044 clear: both;
3045 min-height: 10px;
3045 min-height: 10px;
3046 }
3046 }
3047
3047
3048 .user-group-hovercard {
3048 .user-group-hovercard {
3049 padding: 5px;
3049 padding: 5px;
3050 }
3050 }
3051
3051
3052 .user-group-hovercard-icon {
3052 .user-group-hovercard-icon {
3053 display: inline;
3053 display: inline;
3054 padding: 0;
3054 padding: 0;
3055 box-sizing: content-box;
3055 box-sizing: content-box;
3056 border-radius: 50%;
3056 border-radius: 50%;
3057 float: left;
3057 float: left;
3058 }
3058 }
3059
3059
3060 .user-group-hovercard-name {
3060 .user-group-hovercard-name {
3061 float: left;
3061 float: left;
3062 vertical-align: top;
3062 vertical-align: top;
3063 padding-left: 10px;
3063 padding-left: 10px;
3064 min-width: 150px;
3064 min-width: 150px;
3065 }
3065 }
3066
3066
3067 .user-group-hovercard-icon i {
3067 .user-group-hovercard-icon i {
3068 border: 1px solid @grey4;
3068 border: 1px solid @grey4;
3069 border-radius: 4px;
3069 border-radius: 4px;
3070 }
3070 }
3071
3071
3072 .user-group-hovercard-bio {
3072 .user-group-hovercard-bio {
3073 clear: both;
3073 clear: both;
3074 padding-top: 10px;
3074 padding-top: 10px;
3075 line-height: 1.0em;
3075 line-height: 1.0em;
3076 }
3076 }
3077
3077
3078 .user-group-hovercard-header {
3078 .user-group-hovercard-header {
3079 clear: both;
3079 clear: both;
3080 min-height: 10px;
3080 min-height: 10px;
3081 }
3081 }
3082
3082
3083 .user-group-hovercard-footer {
3083 .user-group-hovercard-footer {
3084 clear: both;
3084 clear: both;
3085 min-height: 10px;
3085 min-height: 10px;
3086 }
3086 }
3087
3087
3088 .pr-hovercard-header {
3088 .pr-hovercard-header {
3089 clear: both;
3089 clear: both;
3090 display: block;
3090 display: block;
3091 line-height: 20px;
3091 line-height: 20px;
3092 }
3092 }
3093
3093
3094 .pr-hovercard-user {
3094 .pr-hovercard-user {
3095 display: flex;
3095 display: flex;
3096 align-items: center;
3096 align-items: center;
3097 padding-left: 5px;
3097 padding-left: 5px;
3098 }
3098 }
3099
3099
3100 .pr-hovercard-title {
3100 .pr-hovercard-title {
3101 padding-top: 5px;
3101 padding-top: 5px;
3102 }
3102 }
3103
3103
3104 .action-divider {
3104 .action-divider {
3105 opacity: 0.5;
3105 opacity: 0.5;
3106 }
3106 }
3107
3107
3108 .details-inline-block {
3108 .details-inline-block {
3109 display: inline-block;
3109 display: inline-block;
3110 position: relative;
3110 position: relative;
3111 }
3111 }
3112
3112
3113 .details-inline-block summary {
3113 .details-inline-block summary {
3114 list-style: none;
3114 list-style: none;
3115 }
3115 }
3116
3116
3117 details:not([open]) > :not(summary) {
3117 details:not([open]) > :not(summary) {
3118 display: none !important;
3118 display: none !important;
3119 }
3119 }
3120
3120
3121 .details-reset > summary {
3121 .details-reset > summary {
3122 list-style: none;
3122 list-style: none;
3123 }
3123 }
3124
3124
3125 .details-reset > summary::-webkit-details-marker {
3125 .details-reset > summary::-webkit-details-marker {
3126 display: none;
3126 display: none;
3127 }
3127 }
3128
3128
3129 .details-dropdown {
3129 .details-dropdown {
3130 position: absolute;
3130 position: absolute;
3131 top: 100%;
3131 top: 100%;
3132 width: 185px;
3132 width: 185px;
3133 list-style: none;
3133 list-style: none;
3134 background-color: #fff;
3134 background-color: #fff;
3135 background-clip: padding-box;
3135 background-clip: padding-box;
3136 border: 1px solid @grey5;
3136 border: 1px solid @grey5;
3137 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3137 box-shadow: 0 8px 24px rgba(149, 157, 165, .2);
3138 left: -150px;
3138 left: -150px;
3139 text-align: left;
3139 text-align: left;
3140 z-index: 90;
3140 z-index: 90;
3141 }
3141 }
3142
3142
3143 .dropdown-divider {
3143 .dropdown-divider {
3144 display: block;
3144 display: block;
3145 height: 0;
3145 height: 0;
3146 margin: 8px 0;
3146 margin: 8px 0;
3147 border-top: 1px solid @grey5;
3147 border-top: 1px solid @grey5;
3148 }
3148 }
3149
3149
3150 .dropdown-item {
3150 .dropdown-item {
3151 display: block;
3151 display: block;
3152 padding: 4px 8px 4px 16px;
3152 padding: 4px 8px 4px 16px;
3153 overflow: hidden;
3153 overflow: hidden;
3154 text-overflow: ellipsis;
3154 text-overflow: ellipsis;
3155 white-space: nowrap;
3155 white-space: nowrap;
3156 font-weight: normal;
3156 font-weight: normal;
3157 }
3157 }
3158
3158
3159 .right-sidebar {
3159 .right-sidebar {
3160 position: fixed;
3160 position: fixed;
3161 top: 0px;
3161 top: 0px;
3162 bottom: 0;
3162 bottom: 0;
3163 right: 0;
3163 right: 0;
3164
3164
3165 background: #fafafa;
3165 background: #fafafa;
3166 z-index: 50;
3166 z-index: 50;
3167 }
3167 }
3168
3168
3169 .right-sidebar {
3169 .right-sidebar {
3170 border-left: 1px solid @grey5;
3170 border-left: 1px solid @grey5;
3171 }
3171 }
3172
3172
3173 .right-sidebar.right-sidebar-expanded {
3173 .right-sidebar.right-sidebar-expanded {
3174 width: 300px;
3174 width: 300px;
3175 overflow: scroll;
3175 overflow: scroll;
3176 }
3176 }
3177
3177
3178 .right-sidebar.right-sidebar-collapsed {
3178 .right-sidebar.right-sidebar-collapsed {
3179 width: 40px;
3179 width: 40px;
3180 padding: 0;
3180 padding: 0;
3181 display: block;
3181 display: block;
3182 overflow: hidden;
3182 overflow: hidden;
3183 }
3183 }
3184
3184
3185 .sidenav {
3185 .sidenav {
3186 float: right;
3186 float: right;
3187 will-change: min-height;
3187 will-change: min-height;
3188 background: #fafafa;
3188 background: #fafafa;
3189 width: 100%;
3189 width: 100%;
3190 }
3190 }
3191
3191
3192 .sidebar-toggle {
3192 .sidebar-toggle {
3193 height: 30px;
3193 height: 30px;
3194 text-align: center;
3194 text-align: center;
3195 margin: 15px 0px 0 0;
3195 margin: 15px 0px 0 0;
3196 }
3196 }
3197
3197
3198 .sidebar-toggle a {
3198 .sidebar-toggle a {
3199
3199
3200 }
3200 }
3201
3201
3202 .sidebar-content {
3202 .sidebar-content {
3203 margin-left: 15px;
3203 margin-left: 15px;
3204 margin-right: 15px;
3204 margin-right: 15px;
3205 }
3205 }
3206
3206
3207 .sidebar-heading {
3207 .sidebar-heading {
3208 font-size: 1.2em;
3208 font-size: 1.2em;
3209 font-weight: 700;
3209 font-weight: 700;
3210 margin-top: 10px;
3210 margin-top: 10px;
3211 }
3211 }
3212
3212
3213 .sidebar-element {
3213 .sidebar-element {
3214 margin-top: 20px;
3214 margin-top: 20px;
3215 }
3215
3216 .icon-draft {
3217 color: @color-draft
3218 }
3219 }
3220
3216
3221
3217 .right-sidebar-collapsed-state {
3222 .right-sidebar-collapsed-state {
3218 display: flex;
3223 display: flex;
3219 flex-direction: column;
3224 flex-direction: column;
3220 justify-content: center;
3225 justify-content: center;
3221 align-items: center;
3226 align-items: center;
3222 padding: 0 10px;
3227 padding: 0 10px;
3223 cursor: pointer;
3228 cursor: pointer;
3224 font-size: 1.3em;
3229 font-size: 1.3em;
3225 margin: 0 -15px;
3230 margin: 0 -15px;
3226 }
3231 }
3227
3232
3228 .right-sidebar-collapsed-state:hover {
3233 .right-sidebar-collapsed-state:hover {
3229 background-color: @grey5;
3234 background-color: @grey5;
3230 }
3235 }
3231
3236
3232 .old-comments-marker {
3237 .old-comments-marker {
3233 text-align: left;
3238 text-align: left;
3234 }
3239 }
3235
3240
3236 .old-comments-marker td {
3241 .old-comments-marker td {
3237 padding-top: 15px;
3242 padding-top: 15px;
3238 border-bottom: 1px solid @grey5;
3243 }
3239 }
@@ -1,154 +1,156 b''
1 // variables for use in all RhodeCode products
1 // variables for use in all RhodeCode products
2
2
3 // FONTS
3 // FONTS
4 //Primary Colors (brand)
4 //Primary Colors (brand)
5 @rcblue: #427cc9; //RhodeCode blue
5 @rcblue: #427cc9; //RhodeCode blue
6 @rcdarkblue: #305b91; //RhodeCode dark blue
6 @rcdarkblue: #305b91; //RhodeCode dark blue
7 @rclightblue: lighten(@rcblue, 30%);
7 @rclightblue: lighten(@rcblue, 30%);
8 @rchighlightblue: lighten(@rcblue, 35%);
8 @rchighlightblue: lighten(@rcblue, 35%);
9
9
10 // Secondary Colors (greyscale)
10 // Secondary Colors (greyscale)
11 @black: #000;
11 @black: #000;
12 @white: #fff;
12 @white: #fff;
13 @grey1: #2B2B2D; //midnight
13 @grey1: #2B2B2D; //midnight
14 @grey2: #5C5C5C; //charcoal
14 @grey2: #5C5C5C; //charcoal
15 @grey3: #7E7F7F; //tungsten
15 @grey3: #7E7F7F; //tungsten
16 @grey4: #949494; //light grey
16 @grey4: #949494; //light grey
17 @grey5: #dbd9da; //greyish
17 @grey5: #dbd9da; //greyish
18 @grey6: #eeeeee; //silver
18 @grey6: #eeeeee; //silver
19 @grey7: #F5F5F5; //light silver
19 @grey7: #F5F5F5; //light silver
20
20
21 // special for navigation
21 // special for navigation
22 @nav-grey: #CDCCCD;
22 @nav-grey: #CDCCCD;
23
23
24 @grey5-alpha: rgba(219, 217, 218, 0.3);
24 @grey5-alpha: rgba(219, 217, 218, 0.3);
25
25
26 // Tertiary Colors
26 // Tertiary Colors
27 @color1: #879938; //olive green
27 @color1: #879938; //olive green
28 @color2: #fcc93a; //bright yellow
28 @color2: #fcc93a; //bright yellow
29 @color3: #ff9e07; //orange-yellow
29 @color3: #ff9e07; //orange-yellow
30 @color4: #fc663a; //bright orange
30 @color4: #fc663a; //bright orange
31 @color5: #d63d44; //signal red
31 @color5: #d63d44; //signal red
32 @color6: #99287c; //violet
32 @color6: #99287c; //violet
33 @color7: #682668; //dark purple
33 @color7: #682668; //dark purple
34 @color8: #194f8e; //dark blue
34 @color8: #194f8e; //dark blue
35
35
36 // Alert Colors (bright)
36 // Alert Colors (bright)
37 @alert1: #0ac878; //bright green
37 @alert1: #0ac878; //bright green
38 @alert2: #e85e4d; //soft red
38 @alert2: #e85e4d; //soft red
39 @alert3: #ffc854; //corn yellow
39 @alert3: #ffc854; //corn yellow
40 @alert4: #84a5d2; //light blue
40 @alert4: #84a5d2; //light blue
41
41
42 // Alert Inner Colors
42 // Alert Inner Colors
43 @alert1-inner: #daf7eb; //bright green
43 @alert1-inner: #daf7eb; //bright green
44 @alert2-inner: #fbdfdb; //soft red
44 @alert2-inner: #fbdfdb; //soft red
45 @alert3-inner: #fff4dd; //corn yellow
45 @alert3-inner: #fff4dd; //corn yellow
46 @alert4-inner: #e6edf6; //light blue
46 @alert4-inner: #e6edf6; //light blue
47
47
48 // Highlight color for lines and colors
48 // Highlight color for lines and colors
49 @comment-highlight-color: #ffd887;
49 @comment-highlight-color: #ffd887;
50 @color-draft: darken(@alert3, 30%);
51 @color-new: darken(@alert1, 5%);
50
52
51 // FONTS
53 // FONTS
52 @basefontsize: 13px;
54 @basefontsize: 13px;
53 @navigation-fontsize: 14px;
55 @navigation-fontsize: 14px;
54 @journal-fontsize: @basefontsize+7px;
56 @journal-fontsize: @basefontsize+7px;
55 @text-color: @grey2;
57 @text-color: @grey2;
56 @repo-title-fontsize: 18px;
58 @repo-title-fontsize: 18px;
57
59
58 @text-regular: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
60 @text-regular: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
59 @text-monospace: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
61 @text-monospace: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
60
62
61 @text-italic: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
63 @text-italic: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
62
64
63 @text-bold: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
65 @text-bold: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
64 @text-bold-weight: 600;
66 @text-bold-weight: 600;
65
67
66 @text-semibold: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
68 @text-semibold: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
67 @text-semibold-weight: 500;
69 @text-semibold-weight: 500;
68
70
69 @text-bold-italic: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
71 @text-bold-italic: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
70 @text-bold-italic-weight: 600;
72 @text-bold-italic-weight: 600;
71
73
72 @text-code: @text-monospace;
74 @text-code: @text-monospace;
73 @text-light: @text-regular;
75 @text-light: @text-regular;
74
76
75
77
76 // Used for .close buttons
78 // Used for .close buttons
77 @text-bootstrap: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
79 @text-bootstrap: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
78
80
79 @panel-title: @basefontsize;
81 @panel-title: @basefontsize;
80 @panel-footer: @basefontsize;
82 @panel-footer: @basefontsize;
81
83
82 // BORDERS
84 // BORDERS
83 @border-thickness: 1px;
85 @border-thickness: 1px;
84 @border-thickness-buttons: 1px;
86 @border-thickness-buttons: 1px;
85 @border-thickness-tags: 1px;
87 @border-thickness-tags: 1px;
86 @border-radius: 2px;
88 @border-radius: 2px;
87 @border-default-color: @grey5;
89 @border-default-color: @grey5;
88 @border-highlight-color: @grey4;
90 @border-highlight-color: @grey4;
89
91
90 // SPACING
92 // SPACING
91 @contentpadding: 15px; //padding on left and right of pages
93 @contentpadding: 15px; //padding on left and right of pages
92 @pagepadding: 40px; //padding on top and bottom of pages
94 @pagepadding: 40px; //padding on top and bottom of pages
93 @menupadding: 12px; //padding for sidebar and content
95 @menupadding: 12px; //padding for sidebar and content
94 @sidebarpadding: 15px; //spacing between sections
96 @sidebarpadding: 15px; //spacing between sections
95 @space: 40px; //spacing between sections
97 @space: 40px; //spacing between sections
96 @padding: 15px; //padding inside modules
98 @padding: 15px; //padding inside modules
97 @textmargin: 20px; //spacing below headers
99 @textmargin: 20px; //spacing below headers
98 @header-padding: 20px;
100 @header-padding: 20px;
99 @panel-padding: @padding;
101 @panel-padding: @padding;
100 @gravatar-size: 16px; // height/width of gravatar w/o border
102 @gravatar-size: 16px; // height/width of gravatar w/o border
101
103
102 // ADMIN
104 // ADMIN
103 @form-max-width: 750px;
105 @form-max-width: 750px;
104
106
105 // FORMS (new)
107 // FORMS (new)
106 @border-thickness-inputs: 1px;
108 @border-thickness-inputs: 1px;
107 @input-padding: .6em; //needs to match button padding
109 @input-padding: .6em; //needs to match button padding
108 // TODO: johbo: Needed for working computation of paddings around labels etc.
110 // TODO: johbo: Needed for working computation of paddings around labels etc.
109 // Expected to be replaced once we are done with the form refactoring.
111 // Expected to be replaced once we are done with the form refactoring.
110 @input-padding-px: 12px;
112 @input-padding-px: 12px;
111 @legend-width: 220px;
113 @legend-width: 220px;
112 @form-vertical-margin: 20px;
114 @form-vertical-margin: 20px;
113 @form-check-width: 20px;
115 @form-check-width: 20px;
114 @form-radio-width: 10px;
116 @form-radio-width: 10px;
115 @form-textcolor: @grey3;
117 @form-textcolor: @grey3;
116
118
117 // FORMS
119 // FORMS
118 @label-width: 220px;
120 @label-width: 220px;
119 //TODO: lisa: Eventually we don't need both of these; remove
121 //TODO: lisa: Eventually we don't need both of these; remove
120 // label-width when legend-width is no longer used
122 // label-width when legend-width is no longer used
121
123
122 @input-border-thickness: @border-thickness;
124 @input-border-thickness: @border-thickness;
123 @medium-inline-input-width: 115px;
125 @medium-inline-input-width: 115px;
124 @input-description-minwidth: 300px;
126 @input-description-minwidth: 300px;
125 @label2-width: 200px;
127 @label2-width: 200px;
126 @checkboxes-width: 420px;
128 @checkboxes-width: 420px;
127 @label-summary-minwidth: 80px;
129 @label-summary-minwidth: 80px;
128 @search-form-width: 400px;
130 @search-form-width: 400px;
129 @fields-input-m: 400px;
131 @fields-input-m: 400px;
130 @fields-input-l: 720px;
132 @fields-input-l: 720px;
131
133
132 // BUTTONS
134 // BUTTONS
133 @button-padding: .7em;
135 @button-padding: .7em;
134 @button-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.07);
136 @button-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.07);
135
137
136 // DROPDOWNS
138 // DROPDOWNS
137 @dropdown-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.07);
139 @dropdown-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.07);
138
140
139 // DEFAULT WIDTHS
141 // DEFAULT WIDTHS
140 @wrapper-maxwidth: 1600px;
142 @wrapper-maxwidth: 1600px;
141 @sidebar-width: 145px;
143 @sidebar-width: 145px;
142 @sidebar-all-width: @sidebar-width + 2 * @sidebarpadding;
144 @sidebar-all-width: @sidebar-width + 2 * @sidebarpadding;
143 @sidebar-small-width: 100px;
145 @sidebar-small-width: 100px;
144 @sidebar-small-all-width: @sidebar-small-width + 2 * @sidebarpadding;
146 @sidebar-small-all-width: @sidebar-small-width + 2 * @sidebarpadding;
145 @texteditor-width: 660px;
147 @texteditor-width: 660px;
146 @maincontent-maxwidth: 940px;
148 @maincontent-maxwidth: 940px;
147 @pullrequest-width: 1025px;
149 @pullrequest-width: 1025px;
148 @summary-menu-stats-width: 200px;
150 @summary-menu-stats-width: 200px;
149
151
150 // SCREEN WIDTHS
152 // SCREEN WIDTHS
151 @screen-sm-min: 320px;
153 @screen-sm-min: 320px;
152
154
153 // For Bootstrap
155 // For Bootstrap
154 @panel-border-radius: @border-radius;
156 @panel-border-radius: @border-radius;
@@ -1,205 +1,211 b''
1 import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
1 import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
2 import '../channelstream-connection/channelstream-connection.js';
2 import '../channelstream-connection/channelstream-connection.js';
3 import '../rhodecode-toast/rhodecode-toast.js';
3 import '../rhodecode-toast/rhodecode-toast.js';
4 import '../rhodecode-favicon/rhodecode-favicon.js';
4 import '../rhodecode-favicon/rhodecode-favicon.js';
5
5
6 var ccLog = Logger.get('RhodeCodeApp');
6 var ccLog = Logger.get('RhodeCodeApp');
7 ccLog.setLevel(Logger.OFF);
7 ccLog.setLevel(Logger.OFF);
8
8
9 export class RhodecodeApp extends PolymerElement {
9 export class RhodecodeApp extends PolymerElement {
10
10
11 static get is() {
11 static get is() {
12 return 'rhodecode-app';
12 return 'rhodecode-app';
13 }
13 }
14
14
15 static get template(){
15 static get template(){
16 return html`
16 return html`
17 <channelstream-connection
17 <channelstream-connection
18 id="channelstream-connection"
18 id="channelstream-connection"
19 on-channelstream-listen-message="receivedMessage"
19 on-channelstream-listen-message="receivedMessage"
20 on-channelstream-connected="handleConnected"
20 on-channelstream-connected="handleConnected"
21 on-channelstream-subscribed="handleSubscribed">
21 on-channelstream-subscribed="handleSubscribed">
22 </channelstream-connection>
22 </channelstream-connection>
23 <rhodecode-favicon></rhodecode-favicon>
23 <rhodecode-favicon></rhodecode-favicon>
24 `
24 `
25 }
25 }
26
26
27 connectedCallback() {
27 connectedCallback() {
28 super.connectedCallback();
28 super.connectedCallback();
29 ccLog.debug('rhodeCodeApp created');
29 ccLog.debug('rhodeCodeApp created');
30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
31 $.Topic('/comment').subscribe(this.handleComment.bind(this));
31 $.Topic('/comment').subscribe(this.handleComment.bind(this));
32 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
32 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
33 $.Topic('/connection_controller/subscribe').subscribe(
33 $.Topic('/connection_controller/subscribe').subscribe(
34 this.subscribeToChannelTopic.bind(this)
34 this.subscribeToChannelTopic.bind(this)
35 );
35 );
36
36
37 // this event can be used to coordinate plugins to do their
37 // this event can be used to coordinate plugins to do their
38 // initialization before channelstream is kicked off
38 // initialization before channelstream is kicked off
39 $.Topic('/__MAIN_APP__').publish({});
39 $.Topic('/__MAIN_APP__').publish({});
40
40
41 for (var i = 0; i < alertMessagePayloads.length; i++) {
41 for (var i = 0; i < alertMessagePayloads.length; i++) {
42 $.Topic('/notifications').publish(alertMessagePayloads[i]);
42 $.Topic('/notifications').publish(alertMessagePayloads[i]);
43 }
43 }
44 this.initPlugins();
44 this.initPlugins();
45 // after rest of application loads and topics get fired, launch connection
45 // after rest of application loads and topics get fired, launch connection
46 $(document).ready(function () {
46 $(document).ready(function () {
47 this.kickoffChannelstreamPlugin();
47 this.kickoffChannelstreamPlugin();
48 }.bind(this));
48 }.bind(this));
49 }
49 }
50
50
51 initPlugins() {
51 initPlugins() {
52 for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
52 for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
53 var pluginDef = window.APPLICATION_PLUGINS[i];
53 var pluginDef = window.APPLICATION_PLUGINS[i];
54 if (pluginDef.component) {
54 if (pluginDef.component) {
55 var pluginElem = document.createElement(pluginDef.component);
55 var pluginElem = document.createElement(pluginDef.component);
56 this.shadowRoot.appendChild(pluginElem);
56 this.shadowRoot.appendChild(pluginElem);
57 if (typeof pluginElem.init !== 'undefined') {
57 if (typeof pluginElem.init !== 'undefined') {
58 pluginElem.init();
58 pluginElem.init();
59 }
59 }
60 }
60 }
61 }
61 }
62 }
62 }
63
63
64 /** proxy to channelstream connection */
64 /** proxy to channelstream connection */
65 getChannelStreamConnection() {
65 getChannelStreamConnection() {
66 return this.$['channelstream-connection'];
66 return this.$['channelstream-connection'];
67 }
67 }
68
68
69 handleNotifications(data) {
69 handleNotifications(data) {
70 var elem = document.getElementById('notifications');
70 var elem = document.getElementById('notifications');
71 if (elem) {
71 if (elem) {
72 elem.handleNotification(data);
72 elem.handleNotification(data);
73 }
73 }
74
75 }
74 }
76
75
77 handleComment(data) {
76 handleComment(data) {
78 if (data.message.comment_id) {
77
78 if (data.message.comment_data.length !== 0) {
79 if (window.refreshAllComments !== undefined) {
79 if (window.refreshAllComments !== undefined) {
80 refreshAllComments()
80 refreshAllComments()
81 }
81 }
82 var json_data = data.message.comment_data;
83
84 if (window.commentsController !== undefined) {
85
86 window.commentsController.attachComment(json_data)
87 }
82 }
88 }
83 }
89 }
84
90
85 faviconUpdate(data) {
91 faviconUpdate(data) {
86 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
92 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
87 }
93 }
88
94
89 /** opens connection to ws server */
95 /** opens connection to ws server */
90 kickoffChannelstreamPlugin(data) {
96 kickoffChannelstreamPlugin(data) {
91 ccLog.debug('kickoffChannelstreamPlugin');
97 ccLog.debug('kickoffChannelstreamPlugin');
92 var channels = ['broadcast'];
98 var channels = ['broadcast'];
93 var addChannels = this.checkViewChannels();
99 var addChannels = this.checkViewChannels();
94 for (var i = 0; i < addChannels.length; i++) {
100 for (var i = 0; i < addChannels.length; i++) {
95 channels.push(addChannels[i]);
101 channels.push(addChannels[i]);
96 }
102 }
97 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled) {
103 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled) {
98 var channelstreamConnection = this.getChannelStreamConnection();
104 var channelstreamConnection = this.getChannelStreamConnection();
99 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
105 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
100 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
106 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
101 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
107 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
102 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
108 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
103 // some channels might already be registered by topic
109 // some channels might already be registered by topic
104 for (var i = 0; i < channels.length; i++) {
110 for (var i = 0; i < channels.length; i++) {
105 channelstreamConnection.push('channels', channels[i]);
111 channelstreamConnection.push('channels', channels[i]);
106 }
112 }
107 // append any additional channels registered in other plugins
113 // append any additional channels registered in other plugins
108 $.Topic('/connection_controller/subscribe').processPrepared();
114 $.Topic('/connection_controller/subscribe').processPrepared();
109
115
110 channelstreamConnection.connect();
116 channelstreamConnection.connect();
111 }
117 }
112 }
118 }
113
119
114 checkViewChannels() {
120 checkViewChannels() {
115 // subscribe to different channels data is sent.
121 // subscribe to different channels data is sent.
116
122
117 var channels = [];
123 var channels = [];
118 // subscribe to PR repo channel for PR's'
124 // subscribe to PR repo channel for PR's'
119 if (templateContext.pull_request_data.pull_request_id) {
125 if (templateContext.pull_request_data.pull_request_id) {
120 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
126 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
121 String(templateContext.pull_request_data.pull_request_id);
127 String(templateContext.pull_request_data.pull_request_id);
122 channels.push(channelName);
128 channels.push(channelName);
123 }
129 }
124
130
125 if (templateContext.commit_data.commit_id) {
131 if (templateContext.commit_data.commit_id) {
126 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
132 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
127 String(templateContext.commit_data.commit_id);
133 String(templateContext.commit_data.commit_id);
128 channels.push(channelName);
134 channels.push(channelName);
129 }
135 }
130
136
131 return channels;
137 return channels;
132 }
138 }
133
139
134 /** subscribes users from channels in channelstream */
140 /** subscribes users from channels in channelstream */
135 subscribeToChannelTopic(channels) {
141 subscribeToChannelTopic(channels) {
136 var channelstreamConnection = this.getChannelStreamConnection();
142 var channelstreamConnection = this.getChannelStreamConnection();
137 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
143 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
138 ccLog.debug('subscribeToChannelTopic', toSubscribe);
144 ccLog.debug('subscribeToChannelTopic', toSubscribe);
139 if (toSubscribe.length > 0) {
145 if (toSubscribe.length > 0) {
140 // if we are connected then subscribe
146 // if we are connected then subscribe
141 if (channelstreamConnection.connected) {
147 if (channelstreamConnection.connected) {
142 channelstreamConnection.subscribe(toSubscribe);
148 channelstreamConnection.subscribe(toSubscribe);
143 }
149 }
144 // not connected? just push channels onto the stack
150 // not connected? just push channels onto the stack
145 else {
151 else {
146 for (var i = 0; i < toSubscribe.length; i++) {
152 for (var i = 0; i < toSubscribe.length; i++) {
147 channelstreamConnection.push('channels', toSubscribe[i]);
153 channelstreamConnection.push('channels', toSubscribe[i]);
148 }
154 }
149 }
155 }
150 }
156 }
151 }
157 }
152
158
153 /** publish received messages into correct topic */
159 /** publish received messages into correct topic */
154 receivedMessage(event) {
160 receivedMessage(event) {
155 for (var i = 0; i < event.detail.length; i++) {
161 for (var i = 0; i < event.detail.length; i++) {
156 var message = event.detail[i];
162 var message = event.detail[i];
157 if (message.message.topic) {
163 if (message.message.topic) {
158 ccLog.debug('publishing', message.message.topic);
164 ccLog.debug('publishing', message.message.topic);
159 $.Topic(message.message.topic).publish(message);
165 $.Topic(message.message.topic).publish(message);
160 }
166 }
161 else if (message.type === 'presence') {
167 else if (message.type === 'presence') {
162 $.Topic('/connection_controller/presence').publish(message);
168 $.Topic('/connection_controller/presence').publish(message);
163 }
169 }
164 else {
170 else {
165 ccLog.warn('unhandled message', message);
171 ccLog.warn('unhandled message', message);
166 }
172 }
167 }
173 }
168 }
174 }
169
175
170 handleConnected(event) {
176 handleConnected(event) {
171 var channelstreamConnection = this.getChannelStreamConnection();
177 var channelstreamConnection = this.getChannelStreamConnection();
172 channelstreamConnection.set('channelsState', event.detail.channels_info);
178 channelstreamConnection.set('channelsState', event.detail.channels_info);
173 channelstreamConnection.set('userState', event.detail.state);
179 channelstreamConnection.set('userState', event.detail.state);
174 channelstreamConnection.set('channels', event.detail.channels);
180 channelstreamConnection.set('channels', event.detail.channels);
175 this.propagageChannelsState();
181 this.propagageChannelsState();
176 }
182 }
177
183
178 handleSubscribed(event) {
184 handleSubscribed(event) {
179 var channelstreamConnection = this.getChannelStreamConnection();
185 var channelstreamConnection = this.getChannelStreamConnection();
180 var channelInfo = event.detail.channels_info;
186 var channelInfo = event.detail.channels_info;
181 var channelKeys = Object.keys(event.detail.channels_info);
187 var channelKeys = Object.keys(event.detail.channels_info);
182 for (var i = 0; i < channelKeys.length; i++) {
188 for (var i = 0; i < channelKeys.length; i++) {
183 var key = channelKeys[i];
189 var key = channelKeys[i];
184 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
190 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
185 }
191 }
186 channelstreamConnection.set('channels', event.detail.channels);
192 channelstreamConnection.set('channels', event.detail.channels);
187 this.propagageChannelsState();
193 this.propagageChannelsState();
188 }
194 }
189
195
190 /** propagates channel states on topics */
196 /** propagates channel states on topics */
191 propagageChannelsState(event) {
197 propagageChannelsState(event) {
192 var channelstreamConnection = this.getChannelStreamConnection();
198 var channelstreamConnection = this.getChannelStreamConnection();
193 var channel_data = channelstreamConnection.channelsState;
199 var channel_data = channelstreamConnection.channelsState;
194 var channels = channelstreamConnection.channels;
200 var channels = channelstreamConnection.channels;
195 for (var i = 0; i < channels.length; i++) {
201 for (var i = 0; i < channels.length; i++) {
196 var key = channels[i];
202 var key = channels[i];
197 $.Topic('/connection_controller/channel_update').publish(
203 $.Topic('/connection_controller/channel_update').publish(
198 {channel: key, state: channel_data[key]}
204 {channel: key, state: channel_data[key]}
199 );
205 );
200 }
206 }
201 }
207 }
202
208
203 }
209 }
204
210
205 customElements.define(RhodecodeApp.is, RhodecodeApp);
211 customElements.define(RhodecodeApp.is, RhodecodeApp);
@@ -1,1349 +1,1450 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84
85 if (!(this instanceof CommentForm)) {
85 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
87 }
88
88
89 // bind the element instance to our Form
89 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
90 $(formElement).get(0).CommentForm = this;
91
91
92 this.withLineNo = function(selector) {
92 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
93 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
94 if (lineNo === undefined) {
95 return selector
95 return selector
96 } else {
96 } else {
97 return selector + '_' + lineNo;
97 return selector + '_' + lineNo;
98 }
98 }
99 };
99 };
100
100
101 this.commitId = commitId;
101 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
102 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
103 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
104 this.initAutocompleteActions = initAutocompleteActions;
105
105
106 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
107 this.previewContainer = this.withLineNo('#preview-container');
108
108
109 this.previewBoxSelector = this.withLineNo('#preview-box');
109 this.previewBoxSelector = this.withLineNo('#preview-box');
110
110
111 this.editButton = this.withLineNo('#edit-btn');
111 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
112 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
114 this.commentType = this.withLineNo('#comment_type');
115
115
116 this.resolvesId = null;
116 this.resolvesId = null;
117 this.resolvesActionId = null;
117 this.resolvesActionId = null;
118
118
119 this.closesPr = '#close_pull_request';
119 this.closesPr = '#close_pull_request';
120
120
121 this.cmBox = this.withLineNo('#text');
121 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
123
124 this.statusChange = this.withLineNo('#change_status');
124 this.statusChange = this.withLineNo('#change_status');
125
125
126 this.submitForm = formElement;
126 this.submitForm = formElement;
127
127
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 this.submitButtonText = this.submitButton.val();
129 this.submitButtonText = this.submitButton.val();
130
130
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
132 this.submitDraftButtonText = this.submitDraftButton.val();
133
133
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 {'repo_name': templateContext.repo_name,
135 {'repo_name': templateContext.repo_name,
136 'commit_id': templateContext.commit_data.commit_id});
136 'commit_id': templateContext.commit_data.commit_id});
137
137
138 if (edit){
138 if (edit){
139 this.submitDraftButton.hide();
139 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
140 this.submitButtonText = _gettext('Update Comment');
141 $(this.commentType).prop('disabled', true);
141 $(this.commentType).prop('disabled', true);
142 $(this.commentType).addClass('disabled');
142 $(this.commentType).addClass('disabled');
143 var editInfo =
143 var editInfo =
144 '';
144 '';
145 $(editInfo).insertBefore($(this.editButton).parent());
145 $(editInfo).insertBefore($(this.editButton).parent());
146 }
146 }
147
147
148 if (resolvesCommentId){
148 if (resolvesCommentId){
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 $(this.commentType).prop('disabled', true);
151 $(this.commentType).prop('disabled', true);
152 $(this.commentType).addClass('disabled');
152 $(this.commentType).addClass('disabled');
153
153
154 // disable select
154 // disable select
155 setTimeout(function() {
155 setTimeout(function() {
156 $(self.statusChange).select2('readonly', true);
156 $(self.statusChange).select2('readonly', true);
157 }, 10);
157 }, 10);
158
158
159 var resolvedInfo = (
159 var resolvedInfo = (
160 '<li class="resolve-action">' +
160 '<li class="resolve-action">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 '</li>'
163 '</li>'
164 ).format(resolvesCommentId, _gettext('resolve comment'));
164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 }
166 }
167
167
168 // based on commitId, or pullRequestId decide where do we submit
168 // based on commitId, or pullRequestId decide where do we submit
169 // out data
169 // out data
170 if (this.commitId){
170 if (this.commitId){
171 var pyurl = 'repo_commit_comment_create';
171 var pyurl = 'repo_commit_comment_create';
172 if(edit){
172 if(edit){
173 pyurl = 'repo_commit_comment_edit';
173 pyurl = 'repo_commit_comment_edit';
174 }
174 }
175 this.submitUrl = pyroutes.url(pyurl,
175 this.submitUrl = pyroutes.url(pyurl,
176 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId,
177 'commit_id': this.commitId,
178 'comment_id': comment_id});
178 'comment_id': comment_id});
179 this.selfUrl = pyroutes.url('repo_commit',
179 this.selfUrl = pyroutes.url('repo_commit',
180 {'repo_name': templateContext.repo_name,
180 {'repo_name': templateContext.repo_name,
181 'commit_id': this.commitId});
181 'commit_id': this.commitId});
182
182
183 } else if (this.pullRequestId) {
183 } else if (this.pullRequestId) {
184 var pyurl = 'pullrequest_comment_create';
184 var pyurl = 'pullrequest_comment_create';
185 if(edit){
185 if(edit){
186 pyurl = 'pullrequest_comment_edit';
186 pyurl = 'pullrequest_comment_edit';
187 }
187 }
188 this.submitUrl = pyroutes.url(pyurl,
188 this.submitUrl = pyroutes.url(pyurl,
189 {'repo_name': templateContext.repo_name,
189 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId,
190 'pull_request_id': this.pullRequestId,
191 'comment_id': comment_id});
191 'comment_id': comment_id});
192 this.selfUrl = pyroutes.url('pullrequest_show',
192 this.selfUrl = pyroutes.url('pullrequest_show',
193 {'repo_name': templateContext.repo_name,
193 {'repo_name': templateContext.repo_name,
194 'pull_request_id': this.pullRequestId});
194 'pull_request_id': this.pullRequestId});
195
195
196 } else {
196 } else {
197 throw new Error(
197 throw new Error(
198 'CommentForm requires pullRequestId, or commitId to be specified.')
198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 }
199 }
200
200
201 // FUNCTIONS and helpers
201 // FUNCTIONS and helpers
202 var self = this;
202 var self = this;
203
203
204 this.isInline = function(){
204 this.isInline = function(){
205 return this.lineNo && this.lineNo != 'general';
205 return this.lineNo && this.lineNo != 'general';
206 };
206 };
207
207
208 this.getCmInstance = function(){
208 this.getCmInstance = function(){
209 return this.cm
209 return this.cm
210 };
210 };
211
211
212 this.setPlaceholder = function(placeholder) {
212 this.setPlaceholder = function(placeholder) {
213 var cm = this.getCmInstance();
213 var cm = this.getCmInstance();
214 if (cm){
214 if (cm){
215 cm.setOption('placeholder', placeholder);
215 cm.setOption('placeholder', placeholder);
216 }
216 }
217 };
217 };
218
218
219 this.getCommentStatus = function() {
219 this.getCommentStatus = function() {
220 return $(this.submitForm).find(this.statusChange).val();
220 return $(this.submitForm).find(this.statusChange).val();
221 };
221 };
222
222
223 this.getCommentType = function() {
223 this.getCommentType = function() {
224 return $(this.submitForm).find(this.commentType).val();
224 return $(this.submitForm).find(this.commentType).val();
225 };
225 };
226
226
227 this.getDraftState = function () {
227 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
229 var data = $(submitterElem).data('isDraft');
230 return data
230 return data
231 }
231 }
232
232
233 this.getResolvesId = function() {
233 this.getResolvesId = function() {
234 return $(this.submitForm).find(this.resolvesId).val() || null;
234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 };
235 };
236
236
237 this.getClosePr = function() {
237 this.getClosePr = function() {
238 return $(this.submitForm).find(this.closesPr).val() || null;
238 return $(this.submitForm).find(this.closesPr).val() || null;
239 };
239 };
240
240
241 this.markCommentResolved = function(resolvedCommentId){
241 this.markCommentResolved = function(resolvedCommentId){
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 };
244 };
245
245
246 this.isAllowedToSubmit = function() {
246 this.isAllowedToSubmit = function() {
247 var commentDisabled = $(this.submitButton).prop('disabled');
247 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
249 return !commentDisabled && !draftDisabled;
250 };
250 };
251
251
252 this.initStatusChangeSelector = function(){
252 this.initStatusChangeSelector = function(){
253 var formatChangeStatus = function(state, escapeMarkup) {
253 var formatChangeStatus = function(state, escapeMarkup) {
254 var originalOption = state.element;
254 var originalOption = state.element;
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 return tmpl
256 return tmpl
257 };
257 };
258 var formatResult = function(result, container, query, escapeMarkup) {
258 var formatResult = function(result, container, query, escapeMarkup) {
259 return formatChangeStatus(result, escapeMarkup);
259 return formatChangeStatus(result, escapeMarkup);
260 };
260 };
261
261
262 var formatSelection = function(data, container, escapeMarkup) {
262 var formatSelection = function(data, container, escapeMarkup) {
263 return formatChangeStatus(data, escapeMarkup);
263 return formatChangeStatus(data, escapeMarkup);
264 };
264 };
265
265
266 $(this.submitForm).find(this.statusChange).select2({
266 $(this.submitForm).find(this.statusChange).select2({
267 placeholder: _gettext('Status Review'),
267 placeholder: _gettext('Status Review'),
268 formatResult: formatResult,
268 formatResult: formatResult,
269 formatSelection: formatSelection,
269 formatSelection: formatSelection,
270 containerCssClass: "drop-menu status_box_menu",
270 containerCssClass: "drop-menu status_box_menu",
271 dropdownCssClass: "drop-menu-dropdown",
271 dropdownCssClass: "drop-menu-dropdown",
272 dropdownAutoWidth: true,
272 dropdownAutoWidth: true,
273 minimumResultsForSearch: -1
273 minimumResultsForSearch: -1
274 });
274 });
275
275
276 $(this.submitForm).find(this.statusChange).on('change', function() {
276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 var status = self.getCommentStatus();
277 var status = self.getCommentStatus();
278
278
279 if (status && !self.isInline()) {
279 if (status && !self.isInline()) {
280 $(self.submitButton).prop('disabled', false);
280 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
282 }
282 }
283
283
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 self.setPlaceholder(placeholderText)
285 self.setPlaceholder(placeholderText)
286 })
286 })
287 };
287 };
288
288
289 // reset the comment form into it's original state
289 // reset the comment form into it's original state
290 this.resetCommentFormState = function(content) {
290 this.resetCommentFormState = function(content) {
291 content = content || '';
291 content = content || '';
292
292
293 $(this.editContainer).show();
293 $(this.editContainer).show();
294 $(this.editButton).parent().addClass('active');
294 $(this.editButton).parent().addClass('active');
295
295
296 $(this.previewContainer).hide();
296 $(this.previewContainer).hide();
297 $(this.previewButton).parent().removeClass('active');
297 $(this.previewButton).parent().removeClass('active');
298
298
299 this.setActionButtonsDisabled(true);
299 this.setActionButtonsDisabled(true);
300 self.cm.setValue(content);
300 self.cm.setValue(content);
301 self.cm.setOption("readOnly", false);
301 self.cm.setOption("readOnly", false);
302
302
303 if (this.resolvesId) {
303 if (this.resolvesId) {
304 // destroy the resolve action
304 // destroy the resolve action
305 $(this.resolvesId).parent().remove();
305 $(this.resolvesId).parent().remove();
306 }
306 }
307 // reset closingPR flag
307 // reset closingPR flag
308 $('.close-pr-input').remove();
308 $('.close-pr-input').remove();
309
309
310 $(this.statusChange).select2('readonly', false);
310 $(this.statusChange).select2('readonly', false);
311 };
311 };
312
312
313 this.globalSubmitSuccessCallback = function(comment){
313 this.globalSubmitSuccessCallback = function(comment){
314 // default behaviour is to call GLOBAL hook, if it's registered.
314 // default behaviour is to call GLOBAL hook, if it's registered.
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 commentFormGlobalSubmitSuccessCallback(comment);
316 commentFormGlobalSubmitSuccessCallback(comment);
317 }
317 }
318 };
318 };
319
319
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 };
322 };
323
323
324 // overwrite a submitHandler, we need to do it for inline comments
324 // overwrite a submitHandler, we need to do it for inline comments
325 this.setHandleFormSubmit = function(callback) {
325 this.setHandleFormSubmit = function(callback) {
326 this.handleFormSubmit = callback;
326 this.handleFormSubmit = callback;
327 };
327 };
328
328
329 // overwrite a submitSuccessHandler
329 // overwrite a submitSuccessHandler
330 this.setGlobalSubmitSuccessCallback = function(callback) {
330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 this.globalSubmitSuccessCallback = callback;
331 this.globalSubmitSuccessCallback = callback;
332 };
332 };
333
333
334 // default handler for for submit for main comments
334 // default handler for for submit for main comments
335 this.handleFormSubmit = function() {
335 this.handleFormSubmit = function() {
336 var text = self.cm.getValue();
336 var text = self.cm.getValue();
337 var status = self.getCommentStatus();
337 var status = self.getCommentStatus();
338 var commentType = self.getCommentType();
338 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
339 var isDraft = self.getDraftState();
340 var resolvesCommentId = self.getResolvesId();
340 var resolvesCommentId = self.getResolvesId();
341 var closePullRequest = self.getClosePr();
341 var closePullRequest = self.getClosePr();
342
342
343 if (text === "" && !status) {
343 if (text === "" && !status) {
344 return;
344 return;
345 }
345 }
346
346
347 var excludeCancelBtn = false;
347 var excludeCancelBtn = false;
348 var submitEvent = true;
348 var submitEvent = true;
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 self.cm.setOption("readOnly", true);
350 self.cm.setOption("readOnly", true);
351
351
352 var postData = {
352 var postData = {
353 'text': text,
353 'text': text,
354 'changeset_status': status,
354 'changeset_status': status,
355 'comment_type': commentType,
355 'comment_type': commentType,
356 'csrf_token': CSRF_TOKEN
356 'csrf_token': CSRF_TOKEN
357 };
357 };
358
358
359 if (resolvesCommentId) {
359 if (resolvesCommentId) {
360 postData['resolves_comment_id'] = resolvesCommentId;
360 postData['resolves_comment_id'] = resolvesCommentId;
361 }
361 }
362
362
363 if (closePullRequest) {
363 if (closePullRequest) {
364 postData['close_pull_request'] = true;
364 postData['close_pull_request'] = true;
365 }
365 }
366
366
367 var submitSuccessCallback = function(o) {
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
368 // reload page if we change status for single commit.
369 // reload page if we change status for single commit.
369 if (status && self.commitId) {
370 if (status && self.commitId) {
370 location.reload(true);
371 location.reload(true);
371 } else {
372 } else {
372 $('#injected_page_comments').append(o.rendered_text);
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 self.attachGeneralComment(json_data)
375
373 self.resetCommentFormState();
376 self.resetCommentFormState();
374 timeagoActivate();
377 timeagoActivate();
375 tooltipActivate();
378 tooltipActivate();
376
379
377 // mark visually which comment was resolved
380 // mark visually which comment was resolved
378 if (resolvesCommentId) {
381 if (resolvesCommentId) {
379 self.markCommentResolved(resolvesCommentId);
382 self.markCommentResolved(resolvesCommentId);
380 }
383 }
381 }
384 }
382
385
383 // run global callback on submit
386 // run global callback on submit
384 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
385
388
386 };
389 };
387 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
388 var prefix = "Error while submitting comment.\n"
391 var prefix = "Error while submitting comment.\n"
389 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
390 ajaxErrorSwal(message);
393 ajaxErrorSwal(message);
391 self.resetCommentFormState(text);
394 self.resetCommentFormState(text);
392 };
395 };
393 self.submitAjaxPOST(
396 self.submitAjaxPOST(
394 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
395 };
398 };
396
399
397 this.previewSuccessCallback = function(o) {
400 this.previewSuccessCallback = function(o) {
398 $(self.previewBoxSelector).html(o);
401 $(self.previewBoxSelector).html(o);
399 $(self.previewBoxSelector).removeClass('unloaded');
402 $(self.previewBoxSelector).removeClass('unloaded');
400
403
401 // swap buttons, making preview active
404 // swap buttons, making preview active
402 $(self.previewButton).parent().addClass('active');
405 $(self.previewButton).parent().addClass('active');
403 $(self.editButton).parent().removeClass('active');
406 $(self.editButton).parent().removeClass('active');
404
407
405 // unlock buttons
408 // unlock buttons
406 self.setActionButtonsDisabled(false);
409 self.setActionButtonsDisabled(false);
407 };
410 };
408
411
409 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
410 excludeCancelBtn = excludeCancelBtn || false;
413 excludeCancelBtn = excludeCancelBtn || false;
411 submitEvent = submitEvent || false;
414 submitEvent = submitEvent || false;
412
415
413 $(this.editButton).prop('disabled', state);
416 $(this.editButton).prop('disabled', state);
414 $(this.previewButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
415
418
416 if (!excludeCancelBtn) {
419 if (!excludeCancelBtn) {
417 $(this.cancelButton).prop('disabled', state);
420 $(this.cancelButton).prop('disabled', state);
418 }
421 }
419
422
420 var submitState = state;
423 var submitState = state;
421 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
422 // if the value of commit review status is set, we allow
425 // if the value of commit review status is set, we allow
423 // submit button, but only on Main form, isInline means inline
426 // submit button, but only on Main form, isInline means inline
424 submitState = false
427 submitState = false
425 }
428 }
426
429
427 $(this.submitButton).prop('disabled', submitState);
430 $(this.submitButton).prop('disabled', submitState);
428 $(this.submitDraftButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
429
432
430 if (submitEvent) {
433 if (submitEvent) {
431 var isDraft = self.getDraftState();
434 var isDraft = self.getDraftState();
432
435
433 if (isDraft) {
436 if (isDraft) {
434 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
435 } else {
438 } else {
436 $(this.submitButton).val(_gettext('Submitting...'));
439 $(this.submitButton).val(_gettext('Submitting...'));
437 }
440 }
438
441
439 } else {
442 } else {
440 $(this.submitButton).val(this.submitButtonText);
443 $(this.submitButton).val(this.submitButtonText);
441 $(this.submitDraftButton).val(this.submitDraftButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
442 }
445 }
443
446
444 };
447 };
445
448
446 // lock preview/edit/submit buttons on load, but exclude cancel button
449 // lock preview/edit/submit buttons on load, but exclude cancel button
447 var excludeCancelBtn = true;
450 var excludeCancelBtn = true;
448 this.setActionButtonsDisabled(true, excludeCancelBtn);
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
449
452
450 // anonymous users don't have access to initialized CM instance
453 // anonymous users don't have access to initialized CM instance
451 if (this.cm !== undefined){
454 if (this.cm !== undefined){
452 this.cm.on('change', function(cMirror) {
455 this.cm.on('change', function(cMirror) {
453 if (cMirror.getValue() === "") {
456 if (cMirror.getValue() === "") {
454 self.setActionButtonsDisabled(true, excludeCancelBtn)
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
455 } else {
458 } else {
456 self.setActionButtonsDisabled(false, excludeCancelBtn)
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
457 }
460 }
458 });
461 });
459 }
462 }
460
463
461 $(this.editButton).on('click', function(e) {
464 $(this.editButton).on('click', function(e) {
462 e.preventDefault();
465 e.preventDefault();
463
466
464 $(self.previewButton).parent().removeClass('active');
467 $(self.previewButton).parent().removeClass('active');
465 $(self.previewContainer).hide();
468 $(self.previewContainer).hide();
466
469
467 $(self.editButton).parent().addClass('active');
470 $(self.editButton).parent().addClass('active');
468 $(self.editContainer).show();
471 $(self.editContainer).show();
469
472
470 });
473 });
471
474
472 $(this.previewButton).on('click', function(e) {
475 $(this.previewButton).on('click', function(e) {
473 e.preventDefault();
476 e.preventDefault();
474 var text = self.cm.getValue();
477 var text = self.cm.getValue();
475
478
476 if (text === "") {
479 if (text === "") {
477 return;
480 return;
478 }
481 }
479
482
480 var postData = {
483 var postData = {
481 'text': text,
484 'text': text,
482 'renderer': templateContext.visual.default_renderer,
485 'renderer': templateContext.visual.default_renderer,
483 'csrf_token': CSRF_TOKEN
486 'csrf_token': CSRF_TOKEN
484 };
487 };
485
488
486 // lock ALL buttons on preview
489 // lock ALL buttons on preview
487 self.setActionButtonsDisabled(true);
490 self.setActionButtonsDisabled(true);
488
491
489 $(self.previewBoxSelector).addClass('unloaded');
492 $(self.previewBoxSelector).addClass('unloaded');
490 $(self.previewBoxSelector).html(_gettext('Loading ...'));
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
491
494
492 $(self.editContainer).hide();
495 $(self.editContainer).hide();
493 $(self.previewContainer).show();
496 $(self.previewContainer).show();
494
497
495 // by default we reset state of comment preserving the text
498 // by default we reset state of comment preserving the text
496 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
497 var prefix = "Error while preview of comment.\n"
500 var prefix = "Error while preview of comment.\n"
498 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
499 ajaxErrorSwal(message);
502 ajaxErrorSwal(message);
500
503
501 self.resetCommentFormState(text)
504 self.resetCommentFormState(text)
502 };
505 };
503 self.submitAjaxPOST(
506 self.submitAjaxPOST(
504 self.previewUrl, postData, self.previewSuccessCallback,
507 self.previewUrl, postData, self.previewSuccessCallback,
505 previewFailCallback);
508 previewFailCallback);
506
509
507 $(self.previewButton).parent().addClass('active');
510 $(self.previewButton).parent().addClass('active');
508 $(self.editButton).parent().removeClass('active');
511 $(self.editButton).parent().removeClass('active');
509 });
512 });
510
513
511 $(this.submitForm).submit(function(e) {
514 $(this.submitForm).submit(function(e) {
512 e.preventDefault();
515 e.preventDefault();
513 var allowedToSubmit = self.isAllowedToSubmit();
516 var allowedToSubmit = self.isAllowedToSubmit();
514 if (!allowedToSubmit){
517 if (!allowedToSubmit){
515 return false;
518 return false;
516 }
519 }
517
520
518 self.handleFormSubmit();
521 self.handleFormSubmit();
519 });
522 });
520
523
521 }
524 }
522
525
523 return CommentForm;
526 return CommentForm;
524 });
527 });
525
528
526 /* selector for comment versions */
529 /* selector for comment versions */
527 var initVersionSelector = function(selector, initialData) {
530 var initVersionSelector = function(selector, initialData) {
528
531
529 var formatResult = function(result, container, query, escapeMarkup) {
532 var formatResult = function(result, container, query, escapeMarkup) {
530
533
531 return renderTemplate('commentVersion', {
534 return renderTemplate('commentVersion', {
532 show_disabled: true,
535 show_disabled: true,
533 version: result.comment_version,
536 version: result.comment_version,
534 user_name: result.comment_author_username,
537 user_name: result.comment_author_username,
535 gravatar_url: result.comment_author_gravatar,
538 gravatar_url: result.comment_author_gravatar,
536 size: 16,
539 size: 16,
537 timeago_component: result.comment_created_on,
540 timeago_component: result.comment_created_on,
538 })
541 })
539 };
542 };
540
543
541 $(selector).select2({
544 $(selector).select2({
542 placeholder: "Edited",
545 placeholder: "Edited",
543 containerCssClass: "drop-menu-comment-history",
546 containerCssClass: "drop-menu-comment-history",
544 dropdownCssClass: "drop-menu-dropdown",
547 dropdownCssClass: "drop-menu-dropdown",
545 dropdownAutoWidth: true,
548 dropdownAutoWidth: true,
546 minimumResultsForSearch: -1,
549 minimumResultsForSearch: -1,
547 data: initialData,
550 data: initialData,
548 formatResult: formatResult,
551 formatResult: formatResult,
549 });
552 });
550
553
551 $(selector).on('select2-selecting', function (e) {
554 $(selector).on('select2-selecting', function (e) {
552 // hide the mast as we later do preventDefault()
555 // hide the mast as we later do preventDefault()
553 $("#select2-drop-mask").click();
556 $("#select2-drop-mask").click();
554 e.preventDefault();
557 e.preventDefault();
555 e.choice.action();
558 e.choice.action();
556 });
559 });
557
560
558 $(selector).on("select2-open", function() {
561 $(selector).on("select2-open", function() {
559 timeagoActivate();
562 timeagoActivate();
560 });
563 });
561 };
564 };
562
565
563 /* comments controller */
566 /* comments controller */
564 var CommentsController = function() {
567 var CommentsController = function() {
565 var mainComment = '#text';
568 var mainComment = '#text';
566 var self = this;
569 var self = this;
567
570
568 this.cancelComment = function (node) {
569 var $node = $(node);
570 var edit = $(this).attr('edit');
571 if (edit) {
572 var $general_comments = null;
573 var $inline_comments = $node.closest('div.inline-comments');
574 if (!$inline_comments.length) {
575 $general_comments = $('#comments');
576 var $comment = $general_comments.parent().find('div.comment:hidden');
577 // show hidden general comment form
578 $('#cb-comment-general-form-placeholder').show();
579 } else {
580 var $comment = $inline_comments.find('div.comment:hidden');
581 }
582 $comment.show();
583 }
584 $node.closest('.comment-inline-form').remove();
585 return false;
586 };
587
588 this.showVersion = function (comment_id, comment_history_id) {
571 this.showVersion = function (comment_id, comment_history_id) {
589
572
590 var historyViewUrl = pyroutes.url(
573 var historyViewUrl = pyroutes.url(
591 'repo_commit_comment_history_view',
574 'repo_commit_comment_history_view',
592 {
575 {
593 'repo_name': templateContext.repo_name,
576 'repo_name': templateContext.repo_name,
594 'commit_id': comment_id,
577 'commit_id': comment_id,
595 'comment_history_id': comment_history_id,
578 'comment_history_id': comment_history_id,
596 }
579 }
597 );
580 );
598 successRenderCommit = function (data) {
581 successRenderCommit = function (data) {
599 SwalNoAnimation.fire({
582 SwalNoAnimation.fire({
600 html: data,
583 html: data,
601 title: '',
584 title: '',
602 });
585 });
603 };
586 };
604 failRenderCommit = function () {
587 failRenderCommit = function () {
605 SwalNoAnimation.fire({
588 SwalNoAnimation.fire({
606 html: 'Error while loading comment history',
589 html: 'Error while loading comment history',
607 title: '',
590 title: '',
608 });
591 });
609 };
592 };
610 _submitAjaxPOST(
593 _submitAjaxPOST(
611 historyViewUrl, {'csrf_token': CSRF_TOKEN},
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
612 successRenderCommit,
595 successRenderCommit,
613 failRenderCommit
596 failRenderCommit
614 );
597 );
615 };
598 };
616
599
617 this.getLineNumber = function(node) {
600 this.getLineNumber = function(node) {
618 var $node = $(node);
601 var $node = $(node);
619 var lineNo = $node.closest('td').attr('data-line-no');
602 var lineNo = $node.closest('td').attr('data-line-no');
620 if (lineNo === undefined && $node.data('commentInline')){
603 if (lineNo === undefined && $node.data('commentInline')){
621 lineNo = $node.data('commentLineNo')
604 lineNo = $node.data('commentLineNo')
622 }
605 }
623
606
624 return lineNo
607 return lineNo
625 };
608 };
626
609
627 this.scrollToComment = function(node, offset, outdated) {
610 this.scrollToComment = function(node, offset, outdated) {
628 if (offset === undefined) {
611 if (offset === undefined) {
629 offset = 0;
612 offset = 0;
630 }
613 }
631 var outdated = outdated || false;
614 var outdated = outdated || false;
632 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
633
616
634 if (!node) {
617 if (!node) {
635 node = $('.comment-selected');
618 node = $('.comment-selected');
636 if (!node.length) {
619 if (!node.length) {
637 node = $('comment-current')
620 node = $('comment-current')
638 }
621 }
639 }
622 }
640
623
641 $wrapper = $(node).closest('div.comment');
624 $wrapper = $(node).closest('div.comment');
642
625
643 // show hidden comment when referenced.
626 // show hidden comment when referenced.
644 if (!$wrapper.is(':visible')){
627 if (!$wrapper.is(':visible')){
645 $wrapper.show();
628 $wrapper.show();
646 }
629 }
647
630
648 $comment = $(node).closest(klass);
631 $comment = $(node).closest(klass);
649 $comments = $(klass);
632 $comments = $(klass);
650
633
651 $('.comment-selected').removeClass('comment-selected');
634 $('.comment-selected').removeClass('comment-selected');
652
635
653 var nextIdx = $(klass).index($comment) + offset;
636 var nextIdx = $(klass).index($comment) + offset;
654 if (nextIdx >= $comments.length) {
637 if (nextIdx >= $comments.length) {
655 nextIdx = 0;
638 nextIdx = 0;
656 }
639 }
657 var $next = $(klass).eq(nextIdx);
640 var $next = $(klass).eq(nextIdx);
658
641
659 var $cb = $next.closest('.cb');
642 var $cb = $next.closest('.cb');
660 $cb.removeClass('cb-collapsed');
643 $cb.removeClass('cb-collapsed');
661
644
662 var $filediffCollapseState = $cb.closest('.filediff').prev();
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
663 $filediffCollapseState.prop('checked', false);
646 $filediffCollapseState.prop('checked', false);
664 $next.addClass('comment-selected');
647 $next.addClass('comment-selected');
665 scrollToElement($next);
648 scrollToElement($next);
666 return false;
649 return false;
667 };
650 };
668
651
669 this.nextComment = function(node) {
652 this.nextComment = function(node) {
670 return self.scrollToComment(node, 1);
653 return self.scrollToComment(node, 1);
671 };
654 };
672
655
673 this.prevComment = function(node) {
656 this.prevComment = function(node) {
674 return self.scrollToComment(node, -1);
657 return self.scrollToComment(node, -1);
675 };
658 };
676
659
677 this.nextOutdatedComment = function(node) {
660 this.nextOutdatedComment = function(node) {
678 return self.scrollToComment(node, 1, true);
661 return self.scrollToComment(node, 1, true);
679 };
662 };
680
663
681 this.prevOutdatedComment = function(node) {
664 this.prevOutdatedComment = function(node) {
682 return self.scrollToComment(node, -1, true);
665 return self.scrollToComment(node, -1, true);
683 };
666 };
684
667
668 this.cancelComment = function (node) {
669 var $node = $(node);
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
672
673 if (edit) {
674 var $general_comments = null;
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
683 $comment.show();
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
691 }
692
693 $node.closest('.comment-inline-form').remove();
694 return false;
695 };
696
685 this._deleteComment = function(node) {
697 this._deleteComment = function(node) {
686 var $node = $(node);
698 var $node = $(node);
687 var $td = $node.closest('td');
699 var $td = $node.closest('td');
688 var $comment = $node.closest('.comment');
700 var $comment = $node.closest('.comment');
689 var comment_id = $($comment).data('commentId');
701 var comment_id = $($comment).data('commentId');
690 var isDraft = $($comment).data('commentDraft');
702 var isDraft = $($comment).data('commentDraft');
691 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
703 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
692 var postData = {
704 var postData = {
693 'csrf_token': CSRF_TOKEN
705 'csrf_token': CSRF_TOKEN
694 };
706 };
695
707
696 $comment.addClass('comment-deleting');
708 $comment.addClass('comment-deleting');
697 $comment.hide('fast');
709 $comment.hide('fast');
698
710
699 var success = function(response) {
711 var success = function(response) {
700 $comment.remove();
712 $comment.remove();
701
713
702 if (window.updateSticky !== undefined) {
714 if (window.updateSticky !== undefined) {
703 // potentially our comments change the active window size, so we
715 // potentially our comments change the active window size, so we
704 // notify sticky elements
716 // notify sticky elements
705 updateSticky()
717 updateSticky()
706 }
718 }
707
719
708 if (window.refreshAllComments !== undefined && !isDraft) {
720 if (window.refreshAllComments !== undefined && !isDraft) {
709 // if we have this handler, run it, and refresh all comments boxes
721 // if we have this handler, run it, and refresh all comments boxes
710 refreshAllComments()
722 refreshAllComments()
711 }
723 }
712 return false;
724 return false;
713 };
725 };
714
726
715 var failure = function(jqXHR, textStatus, errorThrown) {
727 var failure = function(jqXHR, textStatus, errorThrown) {
716 var prefix = "Error while deleting this comment.\n"
728 var prefix = "Error while deleting this comment.\n"
717 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
729 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
718 ajaxErrorSwal(message);
730 ajaxErrorSwal(message);
719
731
720 $comment.show('fast');
732 $comment.show('fast');
721 $comment.removeClass('comment-deleting');
733 $comment.removeClass('comment-deleting');
722 return false;
734 return false;
723 };
735 };
724 ajaxPOST(url, postData, success, failure);
736 ajaxPOST(url, postData, success, failure);
725
737
726
738
727
739
728 }
740 }
729
741
730 this.deleteComment = function(node) {
742 this.deleteComment = function(node) {
731 var $comment = $(node).closest('.comment');
743 var $comment = $(node).closest('.comment');
732 var comment_id = $comment.attr('data-comment-id');
744 var comment_id = $comment.attr('data-comment-id');
733
745
734 SwalNoAnimation.fire({
746 SwalNoAnimation.fire({
735 title: 'Delete this comment?',
747 title: 'Delete this comment?',
736 icon: 'warning',
748 icon: 'warning',
737 showCancelButton: true,
749 showCancelButton: true,
738 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
750 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
739
751
740 }).then(function(result) {
752 }).then(function(result) {
741 if (result.value) {
753 if (result.value) {
742 self._deleteComment(node);
754 self._deleteComment(node);
743 }
755 }
744 })
756 })
745 };
757 };
746
758
747 this._finalizeDrafts = function(commentIds) {
759 this._finalizeDrafts = function(commentIds) {
748 window.finalizeDrafts(commentIds)
760 window.finalizeDrafts(commentIds)
749 }
761 }
750
762
751 this.finalizeDrafts = function(commentIds) {
763 this.finalizeDrafts = function(commentIds) {
752
764
753 SwalNoAnimation.fire({
765 SwalNoAnimation.fire({
754 title: _ngettext('Submit {0} draft comment', 'Submit {0} draft comments', commentIds.length).format(commentIds.length),
766 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
755 icon: 'warning',
767 icon: 'warning',
756 showCancelButton: true,
768 showCancelButton: true,
757 confirmButtonText: _gettext('Yes, finalize drafts'),
769 confirmButtonText: _gettext('Yes, finalize drafts'),
758
770
759 }).then(function(result) {
771 }).then(function(result) {
760 if (result.value) {
772 if (result.value) {
761 self._finalizeDrafts(commentIds);
773 self._finalizeDrafts(commentIds);
762 }
774 }
763 })
775 })
764 };
776 };
765
777
766 this.toggleWideMode = function (node) {
778 this.toggleWideMode = function (node) {
779
767 if ($('#content').hasClass('wrapper')) {
780 if ($('#content').hasClass('wrapper')) {
768 $('#content').removeClass("wrapper");
781 $('#content').removeClass("wrapper");
769 $('#content').addClass("wide-mode-wrapper");
782 $('#content').addClass("wide-mode-wrapper");
770 $(node).addClass('btn-success');
783 $(node).addClass('btn-success');
771 return true
784 return true
772 } else {
785 } else {
773 $('#content').removeClass("wide-mode-wrapper");
786 $('#content').removeClass("wide-mode-wrapper");
774 $('#content').addClass("wrapper");
787 $('#content').addClass("wrapper");
775 $(node).removeClass('btn-success');
788 $(node).removeClass('btn-success');
776 return false
789 return false
777 }
790 }
778
791
779 };
792 };
780
793
781 this.toggleComments = function(node, show) {
794 /**
795 * Turn off/on all comments in file diff
796 */
797 this.toggleDiffComments = function(node) {
798 // Find closes filediff container
782 var $filediff = $(node).closest('.filediff');
799 var $filediff = $(node).closest('.filediff');
800 if ($(node).hasClass('toggle-on')) {
801 var show = false;
802 } else if ($(node).hasClass('toggle-off')) {
803 var show = true;
804 }
805
806 // Toggle each individual comment block, so we can un-toggle single ones
807 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
808 self.toggleLineComments($(val), show)
809 })
810
811 // since we change the height of the diff container that has anchor points for upper
812 // sticky header, we need to tell it to re-calculate those
813 if (window.updateSticky !== undefined) {
814 // potentially our comments change the active window size, so we
815 // notify sticky elements
816 updateSticky()
817 }
818
819 return false;
820 }
821
822 this.toggleLineComments = function(node, show) {
823
824 var trElem = $(node).closest('tr')
825
783 if (show === true) {
826 if (show === true) {
784 $filediff.removeClass('hide-comments');
827 // mark outdated comments as visible before the toggle;
828 $(trElem).find('.comment-outdated').show();
829 $(trElem).removeClass('hide-line-comments');
785 } else if (show === false) {
830 } else if (show === false) {
786 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
831 $(trElem).find('.comment-outdated').hide();
787 $filediff.addClass('hide-comments');
832 $(trElem).addClass('hide-line-comments');
788 } else {
833 } else {
789 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
834 // mark outdated comments as visible before the toggle;
790 $filediff.toggleClass('hide-comments');
835 $(trElem).find('.comment-outdated').show();
836 $(trElem).toggleClass('hide-line-comments');
791 }
837 }
792
838
793 // since we change the height of the diff container that has anchor points for upper
839 // since we change the height of the diff container that has anchor points for upper
794 // sticky header, we need to tell it to re-calculate those
840 // sticky header, we need to tell it to re-calculate those
795 if (window.updateSticky !== undefined) {
841 if (window.updateSticky !== undefined) {
796 // potentially our comments change the active window size, so we
842 // potentially our comments change the active window size, so we
797 // notify sticky elements
843 // notify sticky elements
798 updateSticky()
844 updateSticky()
799 }
845 }
800
846
801 return false;
802 };
803
804 this.toggleLineComments = function(node) {
805 self.toggleComments(node, true);
806 var $node = $(node);
807 // mark outdated comments as visible before the toggle;
808 $(node.closest('tr')).find('.comment-outdated').show();
809 $node.closest('tr').toggleClass('hide-line-comments');
810 };
847 };
811
848
812 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
849 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
813 var pullRequestId = templateContext.pull_request_data.pull_request_id;
850 var pullRequestId = templateContext.pull_request_data.pull_request_id;
814 var commitId = templateContext.commit_data.commit_id;
851 var commitId = templateContext.commit_data.commit_id;
815
852
816 var commentForm = new CommentForm(
853 var commentForm = new CommentForm(
817 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
854 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
818 var cm = commentForm.getCmInstance();
855 var cm = commentForm.getCmInstance();
819
856
820 if (resolvesCommentId){
857 if (resolvesCommentId){
821 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
858 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
822 }
859 }
823
860
824 setTimeout(function() {
861 setTimeout(function() {
825 // callbacks
862 // callbacks
826 if (cm !== undefined) {
863 if (cm !== undefined) {
827 commentForm.setPlaceholder(placeholderText);
864 commentForm.setPlaceholder(placeholderText);
828 if (commentForm.isInline()) {
865 if (commentForm.isInline()) {
829 cm.focus();
866 cm.focus();
830 cm.refresh();
867 cm.refresh();
831 }
868 }
832 }
869 }
833 }, 10);
870 }, 10);
834
871
835 // trigger scrolldown to the resolve comment, since it might be away
872 // trigger scrolldown to the resolve comment, since it might be away
836 // from the clicked
873 // from the clicked
837 if (resolvesCommentId){
874 if (resolvesCommentId){
838 var actionNode = $(commentForm.resolvesActionId).offset();
875 var actionNode = $(commentForm.resolvesActionId).offset();
839
876
840 setTimeout(function() {
877 setTimeout(function() {
841 if (actionNode) {
878 if (actionNode) {
842 $('body, html').animate({scrollTop: actionNode.top}, 10);
879 $('body, html').animate({scrollTop: actionNode.top}, 10);
843 }
880 }
844 }, 100);
881 }, 100);
845 }
882 }
846
883
847 // add dropzone support
884 // add dropzone support
848 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
885 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
849 var renderer = templateContext.visual.default_renderer;
886 var renderer = templateContext.visual.default_renderer;
850 if (renderer == 'rst') {
887 if (renderer == 'rst') {
851 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
888 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
852 if (isRendered){
889 if (isRendered){
853 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
890 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
854 }
891 }
855 } else if (renderer == 'markdown') {
892 } else if (renderer == 'markdown') {
856 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
893 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
857 if (isRendered){
894 if (isRendered){
858 attachmentUrl = '!' + attachmentUrl;
895 attachmentUrl = '!' + attachmentUrl;
859 }
896 }
860 } else {
897 } else {
861 var attachmentUrl = '{}'.format(attachmentStoreUrl);
898 var attachmentUrl = '{}'.format(attachmentStoreUrl);
862 }
899 }
863 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
900 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
864
901
865 return false;
902 return false;
866 };
903 };
867
904
868 //see: https://www.dropzonejs.com/#configuration
905 //see: https://www.dropzonejs.com/#configuration
869 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
906 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
870 {'repo_name': templateContext.repo_name,
907 {'repo_name': templateContext.repo_name,
871 'commit_id': templateContext.commit_data.commit_id})
908 'commit_id': templateContext.commit_data.commit_id})
872
909
873 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
910 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
874 if (previewTmpl !== undefined){
911 if (previewTmpl !== undefined){
875 var selectLink = $(formElement).find('.pick-attachment').get(0);
912 var selectLink = $(formElement).find('.pick-attachment').get(0);
876 $(formElement).find('.comment-attachment-uploader').dropzone({
913 $(formElement).find('.comment-attachment-uploader').dropzone({
877 url: storeUrl,
914 url: storeUrl,
878 headers: {"X-CSRF-Token": CSRF_TOKEN},
915 headers: {"X-CSRF-Token": CSRF_TOKEN},
879 paramName: function () {
916 paramName: function () {
880 return "attachment"
917 return "attachment"
881 }, // The name that will be used to transfer the file
918 }, // The name that will be used to transfer the file
882 clickable: selectLink,
919 clickable: selectLink,
883 parallelUploads: 1,
920 parallelUploads: 1,
884 maxFiles: 10,
921 maxFiles: 10,
885 maxFilesize: templateContext.attachment_store.max_file_size_mb,
922 maxFilesize: templateContext.attachment_store.max_file_size_mb,
886 uploadMultiple: false,
923 uploadMultiple: false,
887 autoProcessQueue: true, // if false queue will not be processed automatically.
924 autoProcessQueue: true, // if false queue will not be processed automatically.
888 createImageThumbnails: false,
925 createImageThumbnails: false,
889 previewTemplate: previewTmpl.innerHTML,
926 previewTemplate: previewTmpl.innerHTML,
890
927
891 accept: function (file, done) {
928 accept: function (file, done) {
892 done();
929 done();
893 },
930 },
894 init: function () {
931 init: function () {
895
932
896 this.on("sending", function (file, xhr, formData) {
933 this.on("sending", function (file, xhr, formData) {
897 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
934 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
898 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
935 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
899 });
936 });
900
937
901 this.on("success", function (file, response) {
938 this.on("success", function (file, response) {
902 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
939 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
903 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
940 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
904
941
905 var isRendered = false;
942 var isRendered = false;
906 var ext = file.name.split('.').pop();
943 var ext = file.name.split('.').pop();
907 var imageExts = templateContext.attachment_store.image_ext;
944 var imageExts = templateContext.attachment_store.image_ext;
908 if (imageExts.indexOf(ext) !== -1){
945 if (imageExts.indexOf(ext) !== -1){
909 isRendered = true;
946 isRendered = true;
910 }
947 }
911
948
912 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
949 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
913 });
950 });
914
951
915 this.on("error", function (file, errorMessage, xhr) {
952 this.on("error", function (file, errorMessage, xhr) {
916 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
953 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
917
954
918 var error = null;
955 var error = null;
919
956
920 if (xhr !== undefined){
957 if (xhr !== undefined){
921 var httpStatus = xhr.status + " " + xhr.statusText;
958 var httpStatus = xhr.status + " " + xhr.statusText;
922 if (xhr !== undefined && xhr.status >= 500) {
959 if (xhr !== undefined && xhr.status >= 500) {
923 error = httpStatus;
960 error = httpStatus;
924 }
961 }
925 }
962 }
926
963
927 if (error === null) {
964 if (error === null) {
928 error = errorMessage.error || errorMessage || httpStatus;
965 error = errorMessage.error || errorMessage || httpStatus;
929 }
966 }
930 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
967 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
931
968
932 });
969 });
933 }
970 }
934 });
971 });
935 }
972 }
936 return commentForm;
973 return commentForm;
937 };
974 };
938
975
939 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
976 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
940
977
941 var tmpl = $('#cb-comment-general-form-template').html();
978 var tmpl = $('#cb-comment-general-form-template').html();
942 tmpl = tmpl.format(null, 'general');
979 tmpl = tmpl.format(null, 'general');
943 var $form = $(tmpl);
980 var $form = $(tmpl);
944
981
945 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
982 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
946 var curForm = $formPlaceholder.find('form');
983 var curForm = $formPlaceholder.find('form');
947 if (curForm){
984 if (curForm){
948 curForm.remove();
985 curForm.remove();
949 }
986 }
950 $formPlaceholder.append($form);
987 $formPlaceholder.append($form);
951
988
952 var _form = $($form[0]);
989 var _form = $($form[0]);
953 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
990 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
954 var edit = false;
991 var edit = false;
955 var comment_id = null;
992 var comment_id = null;
956 var commentForm = this.createCommentForm(
993 var commentForm = this.createCommentForm(
957 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
994 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
958 commentForm.initStatusChangeSelector();
995 commentForm.initStatusChangeSelector();
959
996
960 return commentForm;
997 return commentForm;
961 };
998 };
962
999
963 this.editComment = function(node) {
1000 this.editComment = function(node, line_no, f_path) {
1001 self.edit = true;
964 var $node = $(node);
1002 var $node = $(node);
1003 var $td = $node.closest('td');
1004
965 var $comment = $(node).closest('.comment');
1005 var $comment = $(node).closest('.comment');
966 var comment_id = $($comment).data('commentId');
1006 var comment_id = $($comment).data('commentId');
967 var isDraft = $($comment).data('commentDraft');
1007 var isDraft = $($comment).data('commentDraft');
968 var $form = null
1008 var $editForm = null
969
1009
970 var $comments = $node.closest('div.inline-comments');
1010 var $comments = $node.closest('div.inline-comments');
971 var $general_comments = null;
1011 var $general_comments = null;
972 var lineno = null;
973
1012
974 if($comments.length){
1013 if($comments.length){
975 // inline comments setup
1014 // inline comments setup
976 $form = $comments.find('.comment-inline-form');
1015 $editForm = $comments.find('.comment-inline-form');
977 lineno = self.getLineNumber(node)
1016 line_no = self.getLineNumber(node)
978 }
1017 }
979 else{
1018 else{
980 // general comments setup
1019 // general comments setup
981 $comments = $('#comments');
1020 $comments = $('#comments');
982 $form = $comments.find('.comment-inline-form');
1021 $editForm = $comments.find('.comment-inline-form');
983 lineno = $comment[0].id
1022 line_no = $comment[0].id
984 $('#cb-comment-general-form-placeholder').hide();
1023 $('#cb-comment-general-form-placeholder').hide();
985 }
1024 }
986
1025
987 this.edit = true;
1026 if ($editForm.length === 0) {
988
1027
989 if (!$form.length) {
1028 // unhide all comments if they are hidden for a proper REPLY mode
990
991 var $filediff = $node.closest('.filediff');
1029 var $filediff = $node.closest('.filediff');
992 $filediff.removeClass('hide-comments');
1030 $filediff.removeClass('hide-comments');
993 var f_path = $filediff.attr('data-f-path');
994
995 // create a new HTML from template
996
1031
997 var tmpl = $('#cb-comment-inline-form-template').html();
1032 $editForm = self.createNewFormWrapper(f_path, line_no);
998 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1033 if(f_path && line_no) {
999 $form = $(tmpl);
1034 $editForm.addClass('comment-inline-form-edit')
1000 $comment.after($form)
1035 }
1001
1036
1002 var _form = $($form[0]).find('form');
1037 $comment.after($editForm)
1038
1039 var _form = $($editForm[0]).find('form');
1003 var autocompleteActions = ['as_note',];
1040 var autocompleteActions = ['as_note',];
1004 var commentForm = this.createCommentForm(
1041 var commentForm = this.createCommentForm(
1005 _form, lineno, '', autocompleteActions, resolvesCommentId,
1042 _form, line_no, '', autocompleteActions, resolvesCommentId,
1006 this.edit, comment_id);
1043 this.edit, comment_id);
1007 var old_comment_text_binary = $comment.attr('data-comment-text');
1044 var old_comment_text_binary = $comment.attr('data-comment-text');
1008 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1045 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1009 commentForm.cm.setValue(old_comment_text);
1046 commentForm.cm.setValue(old_comment_text);
1010 $comment.hide();
1047 $comment.hide();
1048 tooltipActivate();
1011
1049
1012 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1050 // set a CUSTOM submit handler for inline comment edit action.
1013 form: _form,
1014 parent: $comments,
1015 lineno: lineno,
1016 f_path: f_path}
1017 );
1018
1019 // set a CUSTOM submit handler for inline comments.
1020 commentForm.setHandleFormSubmit(function(o) {
1051 commentForm.setHandleFormSubmit(function(o) {
1021 var text = commentForm.cm.getValue();
1052 var text = commentForm.cm.getValue();
1022 var commentType = commentForm.getCommentType();
1053 var commentType = commentForm.getCommentType();
1023
1054
1024 if (text === "") {
1055 if (text === "") {
1025 return;
1056 return;
1026 }
1057 }
1027
1058
1028 if (old_comment_text == text) {
1059 if (old_comment_text == text) {
1029 SwalNoAnimation.fire({
1060 SwalNoAnimation.fire({
1030 title: 'Unable to edit comment',
1061 title: 'Unable to edit comment',
1031 html: _gettext('Comment body was not changed.'),
1062 html: _gettext('Comment body was not changed.'),
1032 });
1063 });
1033 return;
1064 return;
1034 }
1065 }
1035 var excludeCancelBtn = false;
1066 var excludeCancelBtn = false;
1036 var submitEvent = true;
1067 var submitEvent = true;
1037 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1068 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1038 commentForm.cm.setOption("readOnly", true);
1069 commentForm.cm.setOption("readOnly", true);
1039
1070
1040 // Read last version known
1071 // Read last version known
1041 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1072 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1042 var version = versionSelector.data('lastVersion');
1073 var version = versionSelector.data('lastVersion');
1043
1074
1044 if (!version) {
1075 if (!version) {
1045 version = 0;
1076 version = 0;
1046 }
1077 }
1047
1078
1048 var postData = {
1079 var postData = {
1049 'text': text,
1080 'text': text,
1050 'f_path': f_path,
1081 'f_path': f_path,
1051 'line': lineno,
1082 'line': line_no,
1052 'comment_type': commentType,
1083 'comment_type': commentType,
1053 'draft': isDraft,
1084 'draft': isDraft,
1054 'version': version,
1085 'version': version,
1055 'csrf_token': CSRF_TOKEN
1086 'csrf_token': CSRF_TOKEN
1056 };
1087 };
1057
1088
1058 var submitSuccessCallback = function(json_data) {
1089 var submitSuccessCallback = function(json_data) {
1059 $form.remove();
1090 $editForm.remove();
1060 $comment.show();
1091 $comment.show();
1061 var postData = {
1092 var postData = {
1062 'text': text,
1093 'text': text,
1063 'renderer': $comment.attr('data-comment-renderer'),
1094 'renderer': $comment.attr('data-comment-renderer'),
1064 'csrf_token': CSRF_TOKEN
1095 'csrf_token': CSRF_TOKEN
1065 };
1096 };
1066
1097
1067 /* Inject new edited version selector */
1098 /* Inject new edited version selector */
1068 var updateCommentVersionDropDown = function () {
1099 var updateCommentVersionDropDown = function () {
1069 var versionSelectId = '#comment_versions_'+comment_id;
1100 var versionSelectId = '#comment_versions_'+comment_id;
1070 var preLoadVersionData = [
1101 var preLoadVersionData = [
1071 {
1102 {
1072 id: json_data['comment_version'],
1103 id: json_data['comment_version'],
1073 text: "v{0}".format(json_data['comment_version']),
1104 text: "v{0}".format(json_data['comment_version']),
1074 action: function () {
1105 action: function () {
1075 Rhodecode.comments.showVersion(
1106 Rhodecode.comments.showVersion(
1076 json_data['comment_id'],
1107 json_data['comment_id'],
1077 json_data['comment_history_id']
1108 json_data['comment_history_id']
1078 )
1109 )
1079 },
1110 },
1080 comment_version: json_data['comment_version'],
1111 comment_version: json_data['comment_version'],
1081 comment_author_username: json_data['comment_author_username'],
1112 comment_author_username: json_data['comment_author_username'],
1082 comment_author_gravatar: json_data['comment_author_gravatar'],
1113 comment_author_gravatar: json_data['comment_author_gravatar'],
1083 comment_created_on: json_data['comment_created_on'],
1114 comment_created_on: json_data['comment_created_on'],
1084 },
1115 },
1085 ]
1116 ]
1086
1117
1087
1118
1088 if ($(versionSelectId).data('select2')) {
1119 if ($(versionSelectId).data('select2')) {
1089 var oldData = $(versionSelectId).data('select2').opts.data.results;
1120 var oldData = $(versionSelectId).data('select2').opts.data.results;
1090 $(versionSelectId).select2("destroy");
1121 $(versionSelectId).select2("destroy");
1091 preLoadVersionData = oldData.concat(preLoadVersionData)
1122 preLoadVersionData = oldData.concat(preLoadVersionData)
1092 }
1123 }
1093
1124
1094 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1125 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1095
1126
1096 $comment.attr('data-comment-text', utf8ToB64(text));
1127 $comment.attr('data-comment-text', utf8ToB64(text));
1097
1128
1098 var versionSelector = $('#comment_versions_'+comment_id);
1129 var versionSelector = $('#comment_versions_'+comment_id);
1099
1130
1100 // set lastVersion so we know our last edit version
1131 // set lastVersion so we know our last edit version
1101 versionSelector.data('lastVersion', json_data['comment_version'])
1132 versionSelector.data('lastVersion', json_data['comment_version'])
1102 versionSelector.parent().show();
1133 versionSelector.parent().show();
1103 }
1134 }
1104 updateCommentVersionDropDown();
1135 updateCommentVersionDropDown();
1105
1136
1106 // by default we reset state of comment preserving the text
1137 // by default we reset state of comment preserving the text
1107 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1138 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1108 var prefix = "Error while editing this comment.\n"
1139 var prefix = "Error while editing this comment.\n"
1109 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1140 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1110 ajaxErrorSwal(message);
1141 ajaxErrorSwal(message);
1111 };
1142 };
1112
1143
1113 var successRenderCommit = function(o){
1144 var successRenderCommit = function(o){
1114 $comment.show();
1145 $comment.show();
1115 $comment[0].lastElementChild.innerHTML = o;
1146 $comment[0].lastElementChild.innerHTML = o;
1116 };
1147 };
1117
1148
1118 var previewUrl = pyroutes.url(
1149 var previewUrl = pyroutes.url(
1119 'repo_commit_comment_preview',
1150 'repo_commit_comment_preview',
1120 {'repo_name': templateContext.repo_name,
1151 {'repo_name': templateContext.repo_name,
1121 'commit_id': templateContext.commit_data.commit_id});
1152 'commit_id': templateContext.commit_data.commit_id});
1122
1153
1123 _submitAjaxPOST(
1154 _submitAjaxPOST(
1124 previewUrl, postData, successRenderCommit,
1155 previewUrl, postData, successRenderCommit, failRenderCommit
1125 failRenderCommit
1126 );
1156 );
1127
1157
1128 try {
1158 try {
1129 var html = json_data.rendered_text;
1159 var html = json_data.rendered_text;
1130 var lineno = json_data.line_no;
1160 var lineno = json_data.line_no;
1131 var target_id = json_data.target_id;
1161 var target_id = json_data.target_id;
1132
1162
1133 $comments.find('.cb-comment-add-button').before(html);
1163 $comments.find('.cb-comment-add-button').before(html);
1134
1164
1135 // run global callback on submit
1165 // run global callback on submit
1136 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1166 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1137
1167
1138 } catch (e) {
1168 } catch (e) {
1139 console.error(e);
1169 console.error(e);
1140 }
1170 }
1141
1171
1142 // re trigger the linkification of next/prev navigation
1172 // re trigger the linkification of next/prev navigation
1143 linkifyComments($('.inline-comment-injected'));
1173 linkifyComments($('.inline-comment-injected'));
1144 timeagoActivate();
1174 timeagoActivate();
1145 tooltipActivate();
1175 tooltipActivate();
1146
1176
1147 if (window.updateSticky !== undefined) {
1177 if (window.updateSticky !== undefined) {
1148 // potentially our comments change the active window size, so we
1178 // potentially our comments change the active window size, so we
1149 // notify sticky elements
1179 // notify sticky elements
1150 updateSticky()
1180 updateSticky()
1151 }
1181 }
1152
1182
1153 if (window.refreshAllComments !== undefined && !isDraft) {
1183 if (window.refreshAllComments !== undefined && !isDraft) {
1154 // if we have this handler, run it, and refresh all comments boxes
1184 // if we have this handler, run it, and refresh all comments boxes
1155 refreshAllComments()
1185 refreshAllComments()
1156 }
1186 }
1157
1187
1158 commentForm.setActionButtonsDisabled(false);
1188 commentForm.setActionButtonsDisabled(false);
1159
1189
1160 };
1190 };
1161
1191
1162 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1192 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1163 var prefix = "Error while editing comment.\n"
1193 var prefix = "Error while editing comment.\n"
1164 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1194 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1165 if (jqXHR.status == 409){
1195 if (jqXHR.status == 409){
1166 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1196 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1167 ajaxErrorSwal(message, 'Comment version mismatch.');
1197 ajaxErrorSwal(message, 'Comment version mismatch.');
1168 } else {
1198 } else {
1169 ajaxErrorSwal(message);
1199 ajaxErrorSwal(message);
1170 }
1200 }
1171
1201
1172 commentForm.resetCommentFormState(text)
1202 commentForm.resetCommentFormState(text)
1173 };
1203 };
1174 commentForm.submitAjaxPOST(
1204 commentForm.submitAjaxPOST(
1175 commentForm.submitUrl, postData,
1205 commentForm.submitUrl, postData,
1176 submitSuccessCallback,
1206 submitSuccessCallback,
1177 submitFailCallback);
1207 submitFailCallback);
1178 });
1208 });
1179 }
1209 }
1180
1210
1181 $form.addClass('comment-inline-form-open');
1211 $editForm.addClass('comment-inline-form-open');
1182 };
1212 };
1183
1213
1184 this.createComment = function(node, resolutionComment) {
1214 this.attachComment = function(json_data) {
1185 var resolvesCommentId = resolutionComment || null;
1215 var self = this;
1216 $.each(json_data, function(idx, val) {
1217 var json_data_elem = [val]
1218 var isInline = val.comment_f_path && val.comment_lineno
1219
1220 if (isInline) {
1221 self.attachInlineComment(json_data_elem)
1222 } else {
1223 self.attachGeneralComment(json_data_elem)
1224 }
1225 })
1226
1227 }
1228
1229 this.attachGeneralComment = function(json_data) {
1230 $.each(json_data, function(idx, val) {
1231 $('#injected_page_comments').append(val.rendered_text);
1232 })
1233 }
1234
1235 this.attachInlineComment = function(json_data) {
1236
1237 $.each(json_data, function (idx, val) {
1238 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1239 var html = val.rendered_text;
1240 var $inlineComments = $('#' + val.target_id)
1241 .find(line_qry)
1242 .find('.inline-comments');
1243
1244 var lastComment = $inlineComments.find('.comment-inline').last();
1245
1246 if (lastComment.length === 0) {
1247 // first comment, we append simply
1248 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1249 } else {
1250 $(lastComment).after(html)
1251 }
1252
1253 })
1254
1255 };
1256
1257 this.createNewFormWrapper = function(f_path, line_no) {
1258 // create a new reply HTML form from template
1259 var tmpl = $('#cb-comment-inline-form-template').html();
1260 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1261 return $(tmpl);
1262 }
1263
1264 this.createComment = function(node, f_path, line_no, resolutionComment) {
1265 self.edit = false;
1186 var $node = $(node);
1266 var $node = $(node);
1187 var $td = $node.closest('td');
1267 var $td = $node.closest('td');
1188 var $form = $td.find('.comment-inline-form');
1268 var resolvesCommentId = resolutionComment || null;
1189 this.edit = false;
1190
1269
1191 if (!$form.length) {
1270 var $replyForm = $td.find('.comment-inline-form');
1192
1271
1193 var $filediff = $node.closest('.filediff');
1272 // if form isn't existing, we're generating a new one and injecting it.
1194 $filediff.removeClass('hide-comments');
1273 if ($replyForm.length === 0) {
1195 var f_path = $filediff.attr('data-f-path');
1274
1196 var lineno = self.getLineNumber(node);
1275 // unhide/expand all comments if they are hidden for a proper REPLY mode
1197 // create a new HTML from template
1276 self.toggleLineComments($node, true);
1198 var tmpl = $('#cb-comment-inline-form-template').html();
1277
1199 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1278 $replyForm = self.createNewFormWrapper(f_path, line_no);
1200 $form = $(tmpl);
1201
1279
1202 var $comments = $td.find('.inline-comments');
1280 var $comments = $td.find('.inline-comments');
1203 if (!$comments.length) {
1281
1204 $comments = $(
1282 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1205 $('#cb-comments-inline-container-template').html());
1283 if ($comments.length===0) {
1206 $td.append($comments);
1284 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1285 var $reply_container = $('#cb-comments-inline-container-template')
1286 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1287 $td.append($($reply_container).html());
1207 }
1288 }
1208
1289
1209 $td.find('.cb-comment-add-button').before($form);
1290 // default comment button exists, so we prepend the form for leaving initial comment
1291 $td.find('.cb-comment-add-button').before($replyForm);
1292 // set marker, that we have a open form
1293 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1294 $replyWrapper.addClass('comment-form-active');
1210
1295
1211 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1296 var lastComment = $comments.find('.comment-inline').last();
1212 var _form = $($form[0]).find('form');
1297 if ($(lastComment).hasClass('comment-outdated')) {
1298 $replyWrapper.show();
1299 }
1300
1301 var _form = $($replyForm[0]).find('form');
1213 var autocompleteActions = ['as_note', 'as_todo'];
1302 var autocompleteActions = ['as_note', 'as_todo'];
1214 var comment_id=null;
1303 var comment_id=null;
1215 var commentForm = this.createCommentForm(
1304 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1216 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1305 var commentForm = self.createCommentForm(
1217
1306 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1218 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1307 self.edit, comment_id);
1219 form: _form,
1220 parent: $td[0],
1221 lineno: lineno,
1222 f_path: f_path}
1223 );
1224
1308
1225 // set a CUSTOM submit handler for inline comments.
1309 // set a CUSTOM submit handler for inline comments.
1226 commentForm.setHandleFormSubmit(function(o) {
1310 commentForm.setHandleFormSubmit(function(o) {
1227 var text = commentForm.cm.getValue();
1311 var text = commentForm.cm.getValue();
1228 var commentType = commentForm.getCommentType();
1312 var commentType = commentForm.getCommentType();
1229 var resolvesCommentId = commentForm.getResolvesId();
1313 var resolvesCommentId = commentForm.getResolvesId();
1230 var isDraft = commentForm.getDraftState();
1314 var isDraft = commentForm.getDraftState();
1231
1315
1232 if (text === "") {
1316 if (text === "") {
1233 return;
1317 return;
1234 }
1318 }
1235
1319
1236 if (lineno === undefined) {
1320 if (line_no === undefined) {
1237 alert('missing line !');
1321 alert('Error: unable to fetch line number for this inline comment !');
1238 return;
1322 return;
1239 }
1323 }
1324
1240 if (f_path === undefined) {
1325 if (f_path === undefined) {
1241 alert('missing file path !');
1326 alert('Error: unable to fetch file path for this inline comment !');
1242 return;
1327 return;
1243 }
1328 }
1244
1329
1245 var excludeCancelBtn = false;
1330 var excludeCancelBtn = false;
1246 var submitEvent = true;
1331 var submitEvent = true;
1247 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1332 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1248 commentForm.cm.setOption("readOnly", true);
1333 commentForm.cm.setOption("readOnly", true);
1249 var postData = {
1334 var postData = {
1250 'text': text,
1335 'text': text,
1251 'f_path': f_path,
1336 'f_path': f_path,
1252 'line': lineno,
1337 'line': line_no,
1253 'comment_type': commentType,
1338 'comment_type': commentType,
1254 'draft': isDraft,
1339 'draft': isDraft,
1255 'csrf_token': CSRF_TOKEN
1340 'csrf_token': CSRF_TOKEN
1256 };
1341 };
1257 if (resolvesCommentId){
1342 if (resolvesCommentId){
1258 postData['resolves_comment_id'] = resolvesCommentId;
1343 postData['resolves_comment_id'] = resolvesCommentId;
1259 }
1344 }
1260
1345
1346 // submitSuccess for inline commits
1261 var submitSuccessCallback = function(json_data) {
1347 var submitSuccessCallback = function(json_data) {
1262 $form.remove();
1348
1349 $replyForm.remove();
1350 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1351
1263 try {
1352 try {
1264 var html = json_data.rendered_text;
1265 var lineno = json_data.line_no;
1266 var target_id = json_data.target_id;
1267
1353
1268 $comments.find('.cb-comment-add-button').before(html);
1354 // inject newly created comments, json_data is {<comment_id>: {}}
1355 self.attachInlineComment(json_data)
1269
1356
1270 //mark visually which comment was resolved
1357 //mark visually which comment was resolved
1271 if (resolvesCommentId) {
1358 if (resolvesCommentId) {
1272 commentForm.markCommentResolved(resolvesCommentId);
1359 commentForm.markCommentResolved(resolvesCommentId);
1273 }
1360 }
1274
1361
1275 // run global callback on submit
1362 // run global callback on submit
1276 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1363 commentForm.globalSubmitSuccessCallback({
1364 draft: isDraft,
1365 comment_id: comment_id
1366 });
1277
1367
1278 } catch (e) {
1368 } catch (e) {
1279 console.error(e);
1369 console.error(e);
1280 }
1370 }
1281
1371
1282 // re trigger the linkification of next/prev navigation
1283 linkifyComments($('.inline-comment-injected'));
1284 timeagoActivate();
1285 tooltipActivate();
1286
1287 if (window.updateSticky !== undefined) {
1372 if (window.updateSticky !== undefined) {
1288 // potentially our comments change the active window size, so we
1373 // potentially our comments change the active window size, so we
1289 // notify sticky elements
1374 // notify sticky elements
1290 updateSticky()
1375 updateSticky()
1291 }
1376 }
1292
1377
1293 if (window.refreshAllComments !== undefined && !isDraft) {
1378 if (window.refreshAllComments !== undefined && !isDraft) {
1294 // if we have this handler, run it, and refresh all comments boxes
1379 // if we have this handler, run it, and refresh all comments boxes
1295 refreshAllComments()
1380 refreshAllComments()
1296 }
1381 }
1297
1382
1298 commentForm.setActionButtonsDisabled(false);
1383 commentForm.setActionButtonsDisabled(false);
1299
1384
1385 // re trigger the linkification of next/prev navigation
1386 linkifyComments($('.inline-comment-injected'));
1387 timeagoActivate();
1388 tooltipActivate();
1300 };
1389 };
1390
1301 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1391 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1302 var prefix = "Error while submitting comment.\n"
1392 var prefix = "Error while submitting comment.\n"
1303 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1393 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1304 ajaxErrorSwal(message);
1394 ajaxErrorSwal(message);
1305 commentForm.resetCommentFormState(text)
1395 commentForm.resetCommentFormState(text)
1306 };
1396 };
1397
1307 commentForm.submitAjaxPOST(
1398 commentForm.submitAjaxPOST(
1308 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1399 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1309 });
1400 });
1310 }
1401 }
1311
1402
1312 $form.addClass('comment-inline-form-open');
1403 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1404 $replyForm.addClass('comment-inline-form-open');
1405 tooltipActivate();
1313 };
1406 };
1314
1407
1315 this.createResolutionComment = function(commentId){
1408 this.createResolutionComment = function(commentId){
1316 // hide the trigger text
1409 // hide the trigger text
1317 $('#resolve-comment-{0}'.format(commentId)).hide();
1410 $('#resolve-comment-{0}'.format(commentId)).hide();
1318
1411
1319 var comment = $('#comment-'+commentId);
1412 var comment = $('#comment-'+commentId);
1320 var commentData = comment.data();
1413 var commentData = comment.data();
1321 if (commentData.commentInline) {
1414 if (commentData.commentInline) {
1322 this.createComment(comment, commentId)
1415 var f_path = commentData.fPath;
1416 var line_no = commentData.lineNo;
1417 //TODO check this if we need to give f_path/line_no
1418 this.createComment(comment, f_path, line_no, commentId)
1323 } else {
1419 } else {
1324 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1420 this.createGeneralComment('general', "$placeholder", commentId)
1325 }
1421 }
1326
1422
1327 return false;
1423 return false;
1328 };
1424 };
1329
1425
1330 this.submitResolution = function(commentId){
1426 this.submitResolution = function(commentId){
1331 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1427 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1332 var commentForm = form.get(0).CommentForm;
1428 var commentForm = form.get(0).CommentForm;
1333
1429
1334 var cm = commentForm.getCmInstance();
1430 var cm = commentForm.getCmInstance();
1335 var renderer = templateContext.visual.default_renderer;
1431 var renderer = templateContext.visual.default_renderer;
1336 if (renderer == 'rst'){
1432 if (renderer == 'rst'){
1337 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1433 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1338 } else if (renderer == 'markdown') {
1434 } else if (renderer == 'markdown') {
1339 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1435 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1340 } else {
1436 } else {
1341 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1437 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1342 }
1438 }
1343
1439
1344 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1440 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1345 form.submit();
1441 form.submit();
1346 return false;
1442 return false;
1347 };
1443 };
1348
1444
1349 };
1445 };
1446
1447 window.commentHelp = function(renderer) {
1448 var funcData = {'renderer': renderer}
1449 return renderTemplate('commentHelpHovercard', funcData)
1450 } No newline at end of file
@@ -1,109 +1,119 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 * QUICK REPO MENU, used on repositories to show shortcuts to files, history
20 * QUICK REPO MENU, used on repositories to show shortcuts to files, history
21 * etc.
21 * etc.
22 */
22 */
23
23
24 var quick_repo_menu = function() {
24 var quick_repo_menu = function() {
25 var hide_quick_repo_menus = function() {
25 var hide_quick_repo_menus = function() {
26 $('.menu_items_container').hide();
26 $('.menu_items_container').hide();
27 $('.active_quick_repo').removeClass('active_quick_repo');
27 $('.active_quick_repo').removeClass('active_quick_repo');
28 };
28 };
29 $('.quick_repo_menu').hover(function() {
29 $('.quick_repo_menu').hover(function() {
30 hide_quick_repo_menus();
30 hide_quick_repo_menus();
31 if (!$(this).hasClass('active_quick_repo')) {
31 if (!$(this).hasClass('active_quick_repo')) {
32 $('.menu_items_container', this).removeClass("hidden").show();
32 $('.menu_items_container', this).removeClass("hidden").show();
33 $(this).addClass('active_quick_repo');
33 $(this).addClass('active_quick_repo');
34 }
34 }
35 }, function() {
35 }, function() {
36 hide_quick_repo_menus();
36 hide_quick_repo_menus();
37 });
37 });
38 };
38 };
39
39
40
40
41 window.toggleElement = function (elem, target) {
41 window.toggleElement = function (elem, target) {
42 var $elem = $(elem);
42 var $elem = $(elem);
43 var $target = $(target);
43 var $target = $(target);
44
44
45 if ($target.is(':visible') || $target.length === 0) {
45 if (target !== undefined) {
46 var show = $target.is(':visible') || $target.length === 0;
47 } else {
48 var show = $elem.hasClass('toggle-off')
49 }
50
51 if (show) {
46 $target.hide();
52 $target.hide();
47 $elem.html($elem.data('toggleOn'))
53 $elem.html($elem.data('toggleOn'))
54 $elem.addClass('toggle-on')
55 $elem.removeClass('toggle-off')
48 } else {
56 } else {
49 $target.show();
57 $target.show();
50 $elem.html($elem.data('toggleOff'))
58 $elem.html($elem.data('toggleOff'))
59 $elem.addClass('toggle-off')
60 $elem.removeClass('toggle-on')
51 }
61 }
52
62
53 return false
63 return false
54 }
64 }
55
65
56 var marginExpVal = '300' // needs a sync with `.right-sidebar.right-sidebar-expanded` value
66 var marginExpVal = '300' // needs a sync with `.right-sidebar.right-sidebar-expanded` value
57 var marginColVal = '40' // needs a sync with `.right-sidebar.right-sidebar-collapsed` value
67 var marginColVal = '40' // needs a sync with `.right-sidebar.right-sidebar-collapsed` value
58
68
59 var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
69 var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
60 var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
70 var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
61
71
62 var updateStickyHeader = function () {
72 var updateStickyHeader = function () {
63 if (window.updateSticky !== undefined) {
73 if (window.updateSticky !== undefined) {
64 // potentially our comments change the active window size, so we
74 // potentially our comments change the active window size, so we
65 // notify sticky elements
75 // notify sticky elements
66 updateSticky()
76 updateSticky()
67 }
77 }
68 }
78 }
69
79
70 var expandSidebar = function () {
80 var expandSidebar = function () {
71 var $sideBar = $('.right-sidebar');
81 var $sideBar = $('.right-sidebar');
72 $('.outerwrapper').css(marginExpanded);
82 $('.outerwrapper').css(marginExpanded);
73 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
83 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
74 $('.right-sidebar-collapsed-state').hide();
84 $('.right-sidebar-collapsed-state').hide();
75 $('.right-sidebar-expanded-state').show();
85 $('.right-sidebar-expanded-state').show();
76 $('.branding').addClass('display-none');
86 $('.branding').addClass('display-none');
77 $sideBar.addClass('right-sidebar-expanded')
87 $sideBar.addClass('right-sidebar-expanded')
78 $sideBar.removeClass('right-sidebar-collapsed')
88 $sideBar.removeClass('right-sidebar-collapsed')
79 }
89 }
80
90
81 var collapseSidebar = function () {
91 var collapseSidebar = function () {
82 var $sideBar = $('.right-sidebar');
92 var $sideBar = $('.right-sidebar');
83 $('.outerwrapper').css(marginCollapsed);
93 $('.outerwrapper').css(marginCollapsed);
84 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
94 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
85 $('.right-sidebar-collapsed-state').show();
95 $('.right-sidebar-collapsed-state').show();
86 $('.right-sidebar-expanded-state').hide();
96 $('.right-sidebar-expanded-state').hide();
87 $('.branding').removeClass('display-none');
97 $('.branding').removeClass('display-none');
88 $sideBar.removeClass('right-sidebar-expanded')
98 $sideBar.removeClass('right-sidebar-expanded')
89 $sideBar.addClass('right-sidebar-collapsed')
99 $sideBar.addClass('right-sidebar-collapsed')
90 }
100 }
91
101
92 window.toggleSidebar = function () {
102 window.toggleSidebar = function () {
93 var $sideBar = $('.right-sidebar');
103 var $sideBar = $('.right-sidebar');
94
104
95 if ($sideBar.hasClass('right-sidebar-expanded')) {
105 if ($sideBar.hasClass('right-sidebar-expanded')) {
96 // expanded -> collapsed transition
106 // expanded -> collapsed transition
97 collapseSidebar();
107 collapseSidebar();
98 var sidebarState = 'collapsed';
108 var sidebarState = 'collapsed';
99
109
100 } else {
110 } else {
101 // collapsed -> expanded
111 // collapsed -> expanded
102 expandSidebar();
112 expandSidebar();
103 var sidebarState = 'expanded';
113 var sidebarState = 'expanded';
104 }
114 }
105
115
106 // update our other sticky header in same context
116 // update our other sticky header in same context
107 updateStickyHeader();
117 updateStickyHeader();
108 storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
118 storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
109 }
119 }
@@ -1,9 +1,7 b''
1 /__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
1 /__MAIN_APP__ - launched when rhodecode-app element is attached to DOM
2 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
2 /plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed
3 /ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview
4 /ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created
5 /notifications - shows new event notifications
3 /notifications - shows new event notifications
6 /connection_controller/subscribe - subscribes user to new channels
4 /connection_controller/subscribe - subscribes user to new channels
7 /connection_controller/presence - receives presence change messages
5 /connection_controller/presence - receives presence change messages
8 /connection_controller/channel_update - receives channel states
6 /connection_controller/channel_update - receives channel states
9 /favicon/update - notify state change for favicon
7 /favicon/update - notify state change for favicon
@@ -1,4 +1,4 b''
1 ## this is a dummy html file for partial rendering on server and sending
1 ## this is a dummy html file for partial rendering on server and sending
2 ## generated output via ajax after comment submit
2 ## generated output via ajax after comment submit
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=c.co.is_inline)}
4 ${comment.comment_block(c.co, inline=c.co.is_inline, is_new=c.is_new)}
@@ -1,542 +1,560 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%!
8 <%!
9 from rhodecode.lib import html_filters
9 from rhodecode.lib import html_filters
10 %>
10 %>
11
11
12
12
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
14
14
15 <%
15 <%
16 from rhodecode.model.comment import CommentsModel
16 from rhodecode.model.comment import CommentsModel
17 comment_model = CommentsModel()
17 comment_model = CommentsModel()
18
18
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
21 visible_for_user = True
21 visible_for_user = True
22 if comment.draft:
22 if comment.draft:
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
24 %>
24 %>
25
25
26 % if inline:
26 % if inline:
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
28 % else:
28 % else:
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
30 % endif
30 % endif
31
31
32 % if visible_for_user:
32 % if visible_for_user:
33 <div class="comment
33 <div class="comment
34 ${'comment-inline' if inline else 'comment-general'}
34 ${'comment-inline' if inline else 'comment-general'}
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
36 id="comment-${comment.comment_id}"
36 id="comment-${comment.comment_id}"
37 line="${comment.line_no}"
37 line="${comment.line_no}"
38 data-comment-id="${comment.comment_id}"
38 data-comment-id="${comment.comment_id}"
39 data-comment-type="${comment.comment_type}"
39 data-comment-type="${comment.comment_type}"
40 data-comment-draft=${h.json.dumps(comment.draft)}
40 data-comment-draft=${h.json.dumps(comment.draft)}
41 data-comment-renderer="${comment.renderer}"
41 data-comment-renderer="${comment.renderer}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
43 data-comment-f-path="${comment.f_path}"
43 data-comment-line-no="${comment.line_no}"
44 data-comment-line-no="${comment.line_no}"
44 data-comment-inline=${h.json.dumps(inline)}
45 data-comment-inline=${h.json.dumps(inline)}
45 style="${'display: none;' if outdated_at_ver else ''}">
46 style="${'display: none;' if outdated_at_ver else ''}">
46
47
47 <div class="meta">
48 <div class="meta">
48 <div class="comment-type-label">
49 <div class="comment-type-label">
49 % if comment.draft:
50 % if comment.draft:
50 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">DRAFT</div>
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 DRAFT
53 </div>
51 % endif
54 % endif
55
56 % if is_new:
57 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
58 NEW
59 </div>
60 % endif
61
52 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
62 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
53
63
54 ## TODO COMMENT
64 ## TODO COMMENT
55 % if comment.comment_type == 'todo':
65 % if comment.comment_type == 'todo':
56 % if comment.resolved:
66 % if comment.resolved:
57 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
67 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
58 <i class="icon-flag-filled"></i>
68 <i class="icon-flag-filled"></i>
59 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
69 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
60 </div>
70 </div>
61 % else:
71 % else:
62 <div class="resolved tooltip" style="display: none">
72 <div class="resolved tooltip" style="display: none">
63 <span>${comment.comment_type}</span>
73 <span>${comment.comment_type}</span>
64 </div>
74 </div>
65 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
75 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
66 <i class="icon-flag-filled"></i>
76 <i class="icon-flag-filled"></i>
67 ${comment.comment_type}
77 ${comment.comment_type}
68 </div>
78 </div>
69 % endif
79 % endif
70 ## NOTE COMMENT
80 ## NOTE COMMENT
71 % else:
81 % else:
72 ## RESOLVED NOTE
82 ## RESOLVED NOTE
73 % if comment.resolved_comment:
83 % if comment.resolved_comment:
74 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
84 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
75 fix
85 fix
76 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
86 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
77 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
87 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
78 </a>
88 </a>
79 </div>
89 </div>
80 ## STATUS CHANGE NOTE
90 ## STATUS CHANGE NOTE
81 % elif not comment.is_inline and comment.status_change:
91 % elif not comment.is_inline and comment.status_change:
82 <%
92 <%
83 if comment.pull_request:
93 if comment.pull_request:
84 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
94 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
85 else:
95 else:
86 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
96 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
87 %>
97 %>
88
98
89 <i class="icon-circle review-status-${comment.review_status}"></i>
99 <i class="icon-circle review-status-${comment.review_status}"></i>
90 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
100 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
91 ${comment.review_status_lbl}
101 ${comment.review_status_lbl}
92 </div>
102 </div>
93 % else:
103 % else:
94 <div>
104 <div>
95 <i class="icon-comment"></i>
105 <i class="icon-comment"></i>
96 ${(comment.comment_type or 'note')}
106 ${(comment.comment_type or 'note')}
97 </div>
107 </div>
98 % endif
108 % endif
99 % endif
109 % endif
100
110
101 </div>
111 </div>
102 </div>
112 </div>
103 ## NOTE 0 and .. => because we disable it for now until UI ready
113 ## NOTE 0 and .. => because we disable it for now until UI ready
104 % if 0 and comment.status_change:
114 % if 0 and comment.status_change:
105 <div class="pull-left">
115 <div class="pull-left">
106 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
116 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
107 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
117 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
108 ${'!{}'.format(comment.pull_request.pull_request_id)}
118 ${'!{}'.format(comment.pull_request.pull_request_id)}
109 </a>
119 </a>
110 </span>
120 </span>
111 </div>
121 </div>
112 % endif
122 % endif
113 ## Since only author can see drafts, we don't show it
123 ## Since only author can see drafts, we don't show it
114 % if not comment.draft:
124 % if not comment.draft:
115 <div class="author ${'author-inline' if inline else 'author-general'}">
125 <div class="author ${'author-inline' if inline else 'author-general'}">
116 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
126 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
117 </div>
127 </div>
118 % endif
128 % endif
119
129
120 <div class="date">
130 <div class="date">
121 ${h.age_component(comment.modified_at, time_is_local=True)}
131 ${h.age_component(comment.modified_at, time_is_local=True)}
122 </div>
132 </div>
123
133
124 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
134 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
125 <span class="tag authortag tooltip" title="${_('Pull request author')}">
135 <span class="tag authortag tooltip" title="${_('Pull request author')}">
126 ${_('author')}
136 ${_('author')}
127 </span>
137 </span>
128 % endif
138 % endif
129
139
130 <%
140 <%
131 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
141 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
132 %>
142 %>
133
143
134 % if comment.history:
144 % if comment.history:
135 <div class="date">
145 <div class="date">
136
146
137 <input id="${comment_version_selector}" name="${comment_version_selector}"
147 <input id="${comment_version_selector}" name="${comment_version_selector}"
138 type="hidden"
148 type="hidden"
139 data-last-version="${comment.history[-1].version}">
149 data-last-version="${comment.history[-1].version}">
140
150
141 <script type="text/javascript">
151 <script type="text/javascript">
142
152
143 var preLoadVersionData = [
153 var preLoadVersionData = [
144 % for comment_history in comment.history:
154 % for comment_history in comment.history:
145 {
155 {
146 id: ${comment_history.comment_history_id},
156 id: ${comment_history.comment_history_id},
147 text: 'v${comment_history.version}',
157 text: 'v${comment_history.version}',
148 action: function () {
158 action: function () {
149 Rhodecode.comments.showVersion(
159 Rhodecode.comments.showVersion(
150 "${comment.comment_id}",
160 "${comment.comment_id}",
151 "${comment_history.comment_history_id}"
161 "${comment_history.comment_history_id}"
152 )
162 )
153 },
163 },
154 comment_version: "${comment_history.version}",
164 comment_version: "${comment_history.version}",
155 comment_author_username: "${comment_history.author.username}",
165 comment_author_username: "${comment_history.author.username}",
156 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
166 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
157 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
167 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
158 },
168 },
159 % endfor
169 % endfor
160 ]
170 ]
161 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
171 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
162
172
163 </script>
173 </script>
164
174
165 </div>
175 </div>
166 % else:
176 % else:
167 <div class="date" style="display: none">
177 <div class="date" style="display: none">
168 <input id="${comment_version_selector}" name="${comment_version_selector}"
178 <input id="${comment_version_selector}" name="${comment_version_selector}"
169 type="hidden"
179 type="hidden"
170 data-last-version="0">
180 data-last-version="0">
171 </div>
181 </div>
172 %endif
182 %endif
173
183
174 <div class="comment-links-block">
184 <div class="comment-links-block">
175
185
176 % if inline:
186 % if inline:
177 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
187 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
178 % if outdated_at_ver:
188 % if outdated_at_ver:
179 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">outdated ${'v{}'.format(comment_ver)}</code>
189 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
180 <code class="action-divider">|</code>
190 <code class="action-divider">|</code>
181 % elif comment_ver:
191 % elif comment_ver:
182 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
192 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
183 <code class="action-divider">|</code>
193 <code class="action-divider">|</code>
184 % endif
194 % endif
185 </a>
195 </a>
186 % else:
196 % else:
187 % if comment_ver:
197 % if comment_ver:
188
198
189 % if comment.outdated:
199 % if comment.outdated:
190 <a class="pr-version"
200 <a class="pr-version"
191 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
201 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
192 >
202 >
193 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
203 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
194 </a>
204 </a>
195 <code class="action-divider">|</code>
205 <code class="action-divider">|</code>
196 % else:
206 % else:
197 <a class="tooltip pr-version"
207 <a class="tooltip pr-version"
198 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
208 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
199 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
209 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
200 >
210 >
201 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
211 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
202 </a>
212 </a>
203 <code class="action-divider">|</code>
213 <code class="action-divider">|</code>
204 % endif
214 % endif
205
215
206 % endif
216 % endif
207 % endif
217 % endif
208
218
209 <details class="details-reset details-inline-block">
219 <details class="details-reset details-inline-block">
210 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
220 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
211 <details-menu class="details-dropdown">
221 <details-menu class="details-dropdown">
212
222
213 <div class="dropdown-item">
223 <div class="dropdown-item">
214 ${_('Comment')} #${comment.comment_id}
224 ${_('Comment')} #${comment.comment_id}
215 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
225 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
216 </div>
226 </div>
217
227
218 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
228 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
219 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
229 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
220 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
230 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
221 ## permissions to delete
231 ## permissions to delete
222 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
232 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
223 <div class="dropdown-divider"></div>
233 <div class="dropdown-divider"></div>
224 <div class="dropdown-item">
234 <div class="dropdown-item">
225 <a onclick="return Rhodecode.comments.editComment(this);" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
235 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
226 </div>
236 </div>
227 <div class="dropdown-item">
237 <div class="dropdown-item">
228 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
238 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
229 </div>
239 </div>
230 % if comment.draft:
240 ## Only available in EE edition
241 % if comment.draft and c.rhodecode_edition_id == 'EE':
231 <div class="dropdown-item">
242 <div class="dropdown-item">
232 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
243 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
233 </div>
244 </div>
234 % endif
245 % endif
235 %else:
246 %else:
236 <div class="dropdown-divider"></div>
247 <div class="dropdown-divider"></div>
237 <div class="dropdown-item">
248 <div class="dropdown-item">
238 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
249 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
239 </div>
250 </div>
240 <div class="dropdown-item">
251 <div class="dropdown-item">
241 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
252 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
242 </div>
253 </div>
243 %endif
254 %endif
244 %else:
255 %else:
245 <div class="dropdown-divider"></div>
256 <div class="dropdown-divider"></div>
246 <div class="dropdown-item">
257 <div class="dropdown-item">
247 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
258 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
248 </div>
259 </div>
249 <div class="dropdown-item">
260 <div class="dropdown-item">
250 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
261 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
251 </div>
262 </div>
252 %endif
263 %endif
253 </details-menu>
264 </details-menu>
254 </details>
265 </details>
255
266
256 <code class="action-divider">|</code>
267 <code class="action-divider">|</code>
257 % if outdated_at_ver:
268 % if outdated_at_ver:
258 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
269 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
259 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
270 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
260 % else:
271 % else:
261 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
272 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
262 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
273 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
263 % endif
274 % endif
264
275
265 </div>
276 </div>
266 </div>
277 </div>
267 <div class="text">
278 <div class="text">
268 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
279 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
269 </div>
280 </div>
270
281
271 </div>
282 </div>
272 % endif
283 % endif
273 </%def>
284 </%def>
274
285
275 ## generate main comments
286 ## generate main comments
276 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
287 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
277 <%
288 <%
278 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
289 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
279 %>
290 %>
280
291
281 <div class="general-comments" id="comments">
292 <div class="general-comments" id="comments">
282 %for comment in comments:
293 %for comment in comments:
283 <div id="comment-tr-${comment.comment_id}">
294 <div id="comment-tr-${comment.comment_id}">
284 ## only render comments that are not from pull request, or from
295 ## only render comments that are not from pull request, or from
285 ## pull request and a status change
296 ## pull request and a status change
286 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
297 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
287 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
298 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
288 %endif
299 %endif
289 </div>
300 </div>
290 %endfor
301 %endfor
291 ## to anchor ajax comments
302 ## to anchor ajax comments
292 <div id="injected_page_comments"></div>
303 <div id="injected_page_comments"></div>
293 </div>
304 </div>
294 </%def>
305 </%def>
295
306
296
307
297 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
308 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
298
309
299 <div class="comments">
310 <div class="comments">
300 <%
311 <%
301 if is_pull_request:
312 if is_pull_request:
302 placeholder = _('Leave a comment on this Pull Request.')
313 placeholder = _('Leave a comment on this Pull Request.')
303 elif is_compare:
314 elif is_compare:
304 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
315 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
305 else:
316 else:
306 placeholder = _('Leave a comment on this Commit.')
317 placeholder = _('Leave a comment on this Commit.')
307 %>
318 %>
308
319
309 % if c.rhodecode_user.username != h.DEFAULT_USER:
320 % if c.rhodecode_user.username != h.DEFAULT_USER:
310 <div class="js-template" id="cb-comment-general-form-template">
321 <div class="js-template" id="cb-comment-general-form-template">
311 ## template generated for injection
322 ## template generated for injection
312 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
323 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
313 </div>
324 </div>
314
325
315 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
326 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
316 ## inject form here
327 ## inject form here
317 </div>
328 </div>
318 <script type="text/javascript">
329 <script type="text/javascript">
319 var lineNo = 'general';
330 var lineNo = 'general';
320 var resolvesCommentId = null;
331 var resolvesCommentId = null;
321 var generalCommentForm = Rhodecode.comments.createGeneralComment(
332 var generalCommentForm = Rhodecode.comments.createGeneralComment(
322 lineNo, "${placeholder}", resolvesCommentId);
333 lineNo, "${placeholder}", resolvesCommentId);
323
334
324 // set custom success callback on rangeCommit
335 // set custom success callback on rangeCommit
325 % if is_compare:
336 % if is_compare:
326 generalCommentForm.setHandleFormSubmit(function(o) {
337 generalCommentForm.setHandleFormSubmit(function(o) {
327 var self = generalCommentForm;
338 var self = generalCommentForm;
328
339
329 var text = self.cm.getValue();
340 var text = self.cm.getValue();
330 var status = self.getCommentStatus();
341 var status = self.getCommentStatus();
331 var commentType = self.getCommentType();
342 var commentType = self.getCommentType();
332 var isDraft = self.getDraftState();
343 var isDraft = self.getDraftState();
333
344
334 if (text === "" && !status) {
345 if (text === "" && !status) {
335 return;
346 return;
336 }
347 }
337
348
338 // we can pick which commits we want to make the comment by
349 // we can pick which commits we want to make the comment by
339 // selecting them via click on preview pane, this will alter the hidden inputs
350 // selecting them via click on preview pane, this will alter the hidden inputs
340 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
351 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
341
352
342 var commitIds = [];
353 var commitIds = [];
343 $('#changeset_compare_view_content .compare_select').each(function(el) {
354 $('#changeset_compare_view_content .compare_select').each(function(el) {
344 var commitId = this.id.replace('row-', '');
355 var commitId = this.id.replace('row-', '');
345 if ($(this).hasClass('hl') || !cherryPicked) {
356 if ($(this).hasClass('hl') || !cherryPicked) {
346 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
357 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
347 commitIds.push(commitId);
358 commitIds.push(commitId);
348 } else {
359 } else {
349 $("input[data-commit-id='{0}']".format(commitId)).val('')
360 $("input[data-commit-id='{0}']".format(commitId)).val('')
350 }
361 }
351 });
362 });
352
363
353 self.setActionButtonsDisabled(true);
364 self.setActionButtonsDisabled(true);
354 self.cm.setOption("readOnly", true);
365 self.cm.setOption("readOnly", true);
355 var postData = {
366 var postData = {
356 'text': text,
367 'text': text,
357 'changeset_status': status,
368 'changeset_status': status,
358 'comment_type': commentType,
369 'comment_type': commentType,
359 'draft': isDraft,
370 'draft': isDraft,
360 'commit_ids': commitIds,
371 'commit_ids': commitIds,
361 'csrf_token': CSRF_TOKEN
372 'csrf_token': CSRF_TOKEN
362 };
373 };
363
374
364 var submitSuccessCallback = function(o) {
375 var submitSuccessCallback = function(o) {
365 location.reload(true);
376 location.reload(true);
366 };
377 };
367 var submitFailCallback = function(){
378 var submitFailCallback = function(){
368 self.resetCommentFormState(text)
379 self.resetCommentFormState(text)
369 };
380 };
370 self.submitAjaxPOST(
381 self.submitAjaxPOST(
371 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
382 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
372 });
383 });
373 % endif
384 % endif
374
385
375 </script>
386 </script>
376 % else:
387 % else:
377 ## form state when not logged in
388 ## form state when not logged in
378 <div class="comment-form ac">
389 <div class="comment-form ac">
379
390
380 <div class="comment-area">
391 <div class="comment-area">
381 <div class="comment-area-header">
392 <div class="comment-area-header">
382 <ul class="nav-links clearfix">
393 <ul class="nav-links clearfix">
383 <li class="active">
394 <li class="active">
384 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
395 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
385 </li>
396 </li>
386 <li class="">
397 <li class="">
387 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
398 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
388 </li>
399 </li>
389 </ul>
400 </ul>
390 </div>
401 </div>
391
402
392 <div class="comment-area-write" style="display: block;">
403 <div class="comment-area-write" style="display: block;">
393 <div id="edit-container">
404 <div id="edit-container">
394 <div style="padding: 40px 0">
405 <div style="padding: 20px 0px 0px 0;">
395 ${_('You need to be logged in to leave comments.')}
406 ${_('You need to be logged in to leave comments.')}
396 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
407 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
397 </div>
408 </div>
398 </div>
409 </div>
399 <div id="preview-container" class="clearfix" style="display: none;">
410 <div id="preview-container" class="clearfix" style="display: none;">
400 <div id="preview-box" class="preview-box"></div>
411 <div id="preview-box" class="preview-box"></div>
401 </div>
412 </div>
402 </div>
413 </div>
403
414
404 <div class="comment-area-footer">
415 <div class="comment-area-footer">
405 <div class="toolbar">
416 <div class="toolbar">
406 <div class="toolbar-text">
417 <div class="toolbar-text">
407 </div>
418 </div>
408 </div>
419 </div>
409 </div>
420 </div>
410 </div>
421 </div>
411
422
412 <div class="comment-footer">
423 <div class="comment-footer">
413 </div>
424 </div>
414
425
415 </div>
426 </div>
416 % endif
427 % endif
417
428
418 <script type="text/javascript">
429 <script type="text/javascript">
419 bindToggleButtons();
430 bindToggleButtons();
420 </script>
431 </script>
421 </div>
432 </div>
422 </%def>
433 </%def>
423
434
424
435
425 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
436 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
426
437
427 ## comment injected based on assumption that user is logged in
438 ## comment injected based on assumption that user is logged in
428 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
439 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
429
440
430 <div class="comment-area">
441 <div class="comment-area">
431 <div class="comment-area-header">
442 <div class="comment-area-header">
432 <div class="pull-left">
443 <div class="pull-left">
433 <ul class="nav-links clearfix">
444 <ul class="nav-links clearfix">
434 <li class="active">
445 <li class="active">
435 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
446 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
436 </li>
447 </li>
437 <li class="">
448 <li class="">
438 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
449 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
439 </li>
450 </li>
440 </ul>
451 </ul>
441 </div>
452 </div>
442 <div class="pull-right">
453 <div class="pull-right">
443 <span class="comment-area-text">${_('Mark as')}:</span>
454 <span class="comment-area-text">${_('Mark as')}:</span>
444 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
455 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
445 % for val in c.visual.comment_types:
456 % for val in c.visual.comment_types:
446 <option value="${val}">${val.upper()}</option>
457 <option value="${val}">${val.upper()}</option>
447 % endfor
458 % endfor
448 </select>
459 </select>
449 </div>
460 </div>
450 </div>
461 </div>
451
462
452 <div class="comment-area-write" style="display: block;">
463 <div class="comment-area-write" style="display: block;">
453 <div id="edit-container_${lineno_id}">
464 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
454 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
465 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
455 </div>
466 </div>
456 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
467 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
457 <div id="preview-box_${lineno_id}" class="preview-box"></div>
468 <div id="preview-box_${lineno_id}" class="preview-box"></div>
458 </div>
469 </div>
459 </div>
470 </div>
460
471
461 <div class="comment-area-footer comment-attachment-uploader">
472 <div class="comment-area-footer comment-attachment-uploader">
462 <div class="toolbar">
473 <div class="toolbar">
463
474
464 <div class="comment-attachment-text">
475 <div class="comment-attachment-text">
465 <div class="dropzone-text">
476 <div class="dropzone-text">
466 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
477 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
467 </div>
478 </div>
468 <div class="dropzone-upload" style="display:none">
479 <div class="dropzone-upload" style="display:none">
469 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
480 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
470 </div>
481 </div>
471 </div>
482 </div>
472
483
473 ## comments dropzone template, empty on purpose
484 ## comments dropzone template, empty on purpose
474 <div style="display: none" class="comment-attachment-uploader-template">
485 <div style="display: none" class="comment-attachment-uploader-template">
475 <div class="dz-file-preview" style="margin: 0">
486 <div class="dz-file-preview" style="margin: 0">
476 <div class="dz-error-message"></div>
487 <div class="dz-error-message"></div>
477 </div>
488 </div>
478 </div>
489 </div>
479
490
480 </div>
491 </div>
481 </div>
492 </div>
482 </div>
493 </div>
483
494
484 <div class="comment-footer">
495 <div class="comment-footer">
485
496
486 ## inject extra inputs into the form
497 ## inject extra inputs into the form
487 % if form_extras and isinstance(form_extras, (list, tuple)):
498 % if form_extras and isinstance(form_extras, (list, tuple)):
488 <div id="comment_form_extras">
499 <div id="comment_form_extras">
489 % for form_ex_el in form_extras:
500 % for form_ex_el in form_extras:
490 ${form_ex_el|n}
501 ${form_ex_el|n}
491 % endfor
502 % endfor
492 </div>
503 </div>
493 % endif
504 % endif
494
505
495 <div class="action-buttons">
506 <div class="action-buttons">
496 % if form_type != 'inline':
507 % if form_type != 'inline':
497 <div class="action-buttons-extra"></div>
508 <div class="action-buttons-extra"></div>
498 % endif
509 % endif
499
510
500 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
511 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
501
512
502 % if form_type == 'inline':
513 % if form_type == 'inline':
514 % if c.rhodecode_edition_id == 'EE':
515 ## Disable the button for CE, the "real" validation is in the backend code anyway
503 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
516 <input class="btn btn-warning comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
517 % else:
518 <input class="tooltip btn btn-warning comment-button-input submit-draft-action disabled" type="submit" disabled="disabled" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
519 % endif
504 % endif
520 % endif
505
521
506 ## inline for has a file, and line-number together with cancel hide button.
507 % if form_type == 'inline':
508 <input type="hidden" name="f_path" value="{0}">
509 <input type="hidden" name="line" value="${lineno_id}">
510 <button type="button" class="tooltip cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);" title="Hide comment box">
511 <i class="icon-cancel-circled2"></i>
512 </button>
513 % endif
514 </div>
515
516 % if review_statuses:
522 % if review_statuses:
517 <div class="status_box">
523 <div class="comment-status-box">
518 <select id="change_status_${lineno_id}" name="changeset_status">
524 <select id="change_status_${lineno_id}" name="changeset_status">
519 <option></option> ## Placeholder
525 <option></option> ## Placeholder
520 % for status, lbl in review_statuses:
526 % for status, lbl in review_statuses:
521 <option value="${status}" data-status="${status}">${lbl}</option>
527 <option value="${status}" data-status="${status}">${lbl}</option>
522 %if is_pull_request and change_status and status in ('approved', 'rejected'):
528 %if is_pull_request and change_status and status in ('approved', 'rejected'):
523 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
529 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
524 %endif
530 %endif
525 % endfor
531 % endfor
526 </select>
532 </select>
527 </div>
533 </div>
528 % endif
534 % endif
529
535
536 ## inline for has a file, and line-number together with cancel hide button.
537 % if form_type == 'inline':
538 <input type="hidden" name="f_path" value="{0}">
539 <input type="hidden" name="line" value="${lineno_id}">
540 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
541 <i class="icon-cancel-circled2"></i>
542 </button>
543 % endif
544 </div>
545
530 <div class="toolbar-text">
546 <div class="toolbar-text">
531 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
547 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
532 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
548 <p>${_('Styling with {} is supported.').format(renderer_url)|n}
533 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
549
534 ${_('and')}
550 <i class="icon-info-circled tooltip-hovercard"
535 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
551 data-hovercard-alt="ALT"
536 ${_('actions supported.')}
552 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
553 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
554 </p>
537 </div>
555 </div>
538 </div>
556 </div>
539
557
540 </form>
558 </form>
541
559
542 </%def> No newline at end of file
560 </%def>
@@ -1,1406 +1,1415 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
3
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 %></%def>
6 %></%def>
6
7
7 <%def name="action_class(action)">
8 <%def name="action_class(action)">
8 <%
9 <%
9 return {
10 return {
10 '-': 'cb-deletion',
11 '-': 'cb-deletion',
11 '+': 'cb-addition',
12 '+': 'cb-addition',
12 ' ': 'cb-context',
13 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
14 }.get(action, 'cb-empty')
14 %>
15 %>
15 </%def>
16 </%def>
16
17
17 <%def name="op_class(op_id)">
18 <%def name="op_class(op_id)">
18 <%
19 <%
19 return {
20 return {
20 DEL_FILENODE: 'deletion', # file deleted
21 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
22 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
23 }.get(op_id, 'addition')
23 %>
24 %>
24 </%def>
25 </%def>
25
26
26
27
27
28
28 <%def name="render_diffset(diffset, commit=None,
29 <%def name="render_diffset(diffset, commit=None,
29
30
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
32 collapse_when_files_over=20,
32
33
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
35 lines_changed_limit=500,
35
36
36 # add a ruler at to the output
37 # add a ruler at to the output
37 ruler_at_chars=0,
38 ruler_at_chars=0,
38
39
39 # show inline comments
40 # show inline comments
40 use_comments=False,
41 use_comments=False,
41
42
42 # disable new comments
43 # disable new comments
43 disable_new_comments=False,
44 disable_new_comments=False,
44
45
45 # special file-comments that were deleted in previous versions
46 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
47 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
48 deleted_files_comments=None,
48
49
49 # for cache purpose
50 # for cache purpose
50 inline_comments=None,
51 inline_comments=None,
51
52
52 # additional menu for PRs
53 # additional menu for PRs
53 pull_request_menu=None,
54 pull_request_menu=None,
54
55
55 # show/hide todo next to comments
56 # show/hide todo next to comments
56 show_todos=True,
57 show_todos=True,
57
58
58 )">
59 )">
59
60
60 <%
61 <%
61 diffset_container_id = h.md5(diffset.target_ref)
62 diffset_container_id = h.md5(diffset.target_ref)
62 collapse_all = len(diffset.files) > collapse_when_files_over
63 collapse_all = len(diffset.files) > collapse_when_files_over
63 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
66 %>
67 %>
67
68
68 %if use_comments:
69 %if use_comments:
69
70
70 ## Template for injecting comments
71 ## Template for injecting comments
71 <div id="cb-comments-inline-container-template" class="js-template">
72 <div id="cb-comments-inline-container-template" class="js-template">
72 ${inline_comments_container([])}
73 ${inline_comments_container([])}
73 </div>
74 </div>
74
75
75 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="comment-inline-form ac">
77 <div class="comment-inline-form ac">
77
78 %if not c.rhodecode_user.is_default:
78 %if c.rhodecode_user.username != h.DEFAULT_USER:
79 ## render template for inline comments
79 ## render template for inline comments
80 ${commentblock.comment_form(form_type='inline')}
80 ${commentblock.comment_form(form_type='inline')}
81 %else:
82 ${h.form('', class_='inline-form comment-form-login', method='get')}
83 <div class="pull-left">
84 <div class="comment-help pull-right">
85 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
86 </div>
87 </div>
88 <div class="comment-button pull-right">
89 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
90 ${_('Cancel')}
91 </button>
92 </div>
93 <div class="clearfix"></div>
94 ${h.end_form()}
95 %endif
81 %endif
96 </div>
82 </div>
97 </div>
83 </div>
98
84
99 %endif
85 %endif
100
86
101 %if c.user_session_attrs["diffmode"] == 'sideside':
87 %if c.user_session_attrs["diffmode"] == 'sideside':
102 <style>
88 <style>
103 .wrapper {
89 .wrapper {
104 max-width: 1600px !important;
90 max-width: 1600px !important;
105 }
91 }
106 </style>
92 </style>
107 %endif
93 %endif
108
94
109 %if ruler_at_chars:
95 %if ruler_at_chars:
110 <style>
96 <style>
111 .diff table.cb .cb-content:after {
97 .diff table.cb .cb-content:after {
112 content: "";
98 content: "";
113 border-left: 1px solid blue;
99 border-left: 1px solid blue;
114 position: absolute;
100 position: absolute;
115 top: 0;
101 top: 0;
116 height: 18px;
102 height: 18px;
117 opacity: .2;
103 opacity: .2;
118 z-index: 10;
104 z-index: 10;
119 //## +5 to account for diff action (+/-)
105 //## +5 to account for diff action (+/-)
120 left: ${ruler_at_chars + 5}ch;
106 left: ${ruler_at_chars + 5}ch;
121 </style>
107 </style>
122 %endif
108 %endif
123
109
124 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
125
111
126 <div style="height: 20px; line-height: 20px">
112 <div style="height: 20px; line-height: 20px">
127 ## expand/collapse action
113 ## expand/collapse action
128 <div class="pull-left">
114 <div class="pull-left">
129 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
130 % if collapse_all:
116 % if collapse_all:
131 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
132 % else:
118 % else:
133 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
134 % endif
120 % endif
135 </a>
121 </a>
136
122
137 </div>
123 </div>
138
124
139 ## todos
125 ## todos
140 % if show_todos and getattr(c, 'at_version', None):
126 % if show_todos and getattr(c, 'at_version', None):
141 <div class="pull-right">
127 <div class="pull-right">
142 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
143 ${_('not available in this view')}
129 ${_('not available in this view')}
144 </div>
130 </div>
145 % elif show_todos:
131 % elif show_todos:
146 <div class="pull-right">
132 <div class="pull-right">
147 <div class="comments-number" style="padding-left: 10px">
133 <div class="comments-number" style="padding-left: 10px">
148 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
149 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
150 % if c.unresolved_comments:
136 % if c.unresolved_comments:
151 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
152 ${_('{} unresolved').format(len(c.unresolved_comments))}
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
153 </a>
139 </a>
154 % else:
140 % else:
155 ${_('0 unresolved')}
141 ${_('0 unresolved')}
156 % endif
142 % endif
157
143
158 ${_('{} Resolved').format(len(c.resolved_comments))}
144 ${_('{} Resolved').format(len(c.resolved_comments))}
159 % endif
145 % endif
160 </div>
146 </div>
161 </div>
147 </div>
162 % endif
148 % endif
163
149
164 ## ## comments
150 ## ## comments
165 ## <div class="pull-right">
151 ## <div class="pull-right">
166 ## <div class="comments-number" style="padding-left: 10px">
152 ## <div class="comments-number" style="padding-left: 10px">
167 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
168 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
169 ## % if c.comments:
155 ## % if c.comments:
170 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
171 ## % else:
157 ## % else:
172 ## ${_('0 General')}
158 ## ${_('0 General')}
173 ## % endif
159 ## % endif
174 ##
160 ##
175 ## % if c.inline_cnt:
161 ## % if c.inline_cnt:
176 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
177 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
178 ## </a>
164 ## </a>
179 ## % else:
165 ## % else:
180 ## ${_('0 Inline')}
166 ## ${_('0 Inline')}
181 ## % endif
167 ## % endif
182 ## % endif
168 ## % endif
183 ##
169 ##
184 ## % if pull_request_menu:
170 ## % if pull_request_menu:
185 ## <%
171 ## <%
186 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
187 ## %>
173 ## %>
188 ##
174 ##
189 ## % if outdated_comm_count_ver:
175 ## % if outdated_comm_count_ver:
190 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
191 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
192 ## </a>
178 ## </a>
193 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
194 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
195 ## % else:
181 ## % else:
196 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
197 ## % endif
183 ## % endif
198 ##
184 ##
199 ## % endif
185 ## % endif
200 ##
186 ##
201 ## </div>
187 ## </div>
202 ## </div>
188 ## </div>
203
189
204 </div>
190 </div>
205
191
206 % if diffset.limited_diff:
192 % if diffset.limited_diff:
207 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
208 <h2 class="clearinner">
194 <h2 class="clearinner">
209 ${_('The requested changes are too big and content was truncated.')}
195 ${_('The requested changes are too big and content was truncated.')}
210 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
211 </h2>
197 </h2>
212 </div>
198 </div>
213 % endif
199 % endif
214
200
215 <div id="todo-box">
201 <div id="todo-box">
216 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
217 % for co in c.unresolved_comments:
203 % for co in c.unresolved_comments:
218 <a class="permalink" href="#comment-${co.comment_id}"
204 <a class="permalink" href="#comment-${co.comment_id}"
219 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
220 <i class="icon-flag-filled-red"></i>
206 <i class="icon-flag-filled-red"></i>
221 ${co.comment_id}</a>${('' if loop.last else ',')}
207 ${co.comment_id}</a>${('' if loop.last else ',')}
222 % endfor
208 % endfor
223 % endif
209 % endif
224 </div>
210 </div>
225 %if diffset.has_hidden_changes:
211 %if diffset.has_hidden_changes:
226 <p class="empty_data">${_('Some changes may be hidden')}</p>
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
227 %elif not diffset.files:
213 %elif not diffset.files:
228 <p class="empty_data">${_('No files')}</p>
214 <p class="empty_data">${_('No files')}</p>
229 %endif
215 %endif
230
216
231 <div class="filediffs">
217 <div class="filediffs">
232
218
233 ## initial value could be marked as False later on
219 ## initial value could be marked as False later on
234 <% over_lines_changed_limit = False %>
220 <% over_lines_changed_limit = False %>
235 %for i, filediff in enumerate(diffset.files):
221 %for i, filediff in enumerate(diffset.files):
236
222
237 %if filediff.source_file_path and filediff.target_file_path:
223 %if filediff.source_file_path and filediff.target_file_path:
238 %if filediff.source_file_path != filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
239 ## file was renamed, or copied
225 ## file was renamed, or copied
240 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
241 <%
227 <%
242 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
243 final_path = filediff.target_file_path
229 final_path = filediff.target_file_path
244 %>
230 %>
245 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
246 <%
232 <%
247 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
248 final_path = filediff.target_file_path
234 final_path = filediff.target_file_path
249 %>
235 %>
250 %endif
236 %endif
251 %else:
237 %else:
252 ## file was modified
238 ## file was modified
253 <%
239 <%
254 final_file_name = filediff.source_file_path
240 final_file_name = filediff.source_file_path
255 final_path = final_file_name
241 final_path = final_file_name
256 %>
242 %>
257 %endif
243 %endif
258 %else:
244 %else:
259 %if filediff.source_file_path:
245 %if filediff.source_file_path:
260 ## file was deleted
246 ## file was deleted
261 <%
247 <%
262 final_file_name = filediff.source_file_path
248 final_file_name = filediff.source_file_path
263 final_path = final_file_name
249 final_path = final_file_name
264 %>
250 %>
265 %else:
251 %else:
266 ## file was added
252 ## file was added
267 <%
253 <%
268 final_file_name = filediff.target_file_path
254 final_file_name = filediff.target_file_path
269 final_path = final_file_name
255 final_path = final_file_name
270 %>
256 %>
271 %endif
257 %endif
272 %endif
258 %endif
273
259
274 <%
260 <%
275 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
276 over_lines_changed_limit = lines_changed > lines_changed_limit
262 over_lines_changed_limit = lines_changed > lines_changed_limit
277 %>
263 %>
278 ## anchor with support of sticky header
264 ## anchor with support of sticky header
279 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
280
266
281 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
282 <div
268 <div
283 class="filediff"
269 class="filediff"
284 data-f-path="${filediff.patch['filename']}"
270 data-f-path="${filediff.patch['filename']}"
285 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
286 >
272 >
287 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
288 <%
274 <%
289 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
290 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not _c.outdated]
291 %>
277 %>
292 <div class="filediff-collapse-indicator icon-"></div>
278 <div class="filediff-collapse-indicator icon-"></div>
293
279
294 ## Comments/Options PILL
280 ## Comments/Options PILL
295 <span class="pill-group pull-right">
281 <span class="pill-group pull-right">
296 <span class="pill" op="comments">
282 <span class="pill" op="comments">
297 <i class="icon-comment"></i> ${len(total_file_comments)}
283 <i class="icon-comment"></i> ${len(total_file_comments)}
298 </span>
284 </span>
299
285
300 <details class="details-reset details-inline-block">
286 <details class="details-reset details-inline-block">
301 <summary class="noselect">
287 <summary class="noselect">
302 <i class="pill icon-options cursor-pointer" op="options"></i>
288 <i class="pill icon-options cursor-pointer" op="options"></i>
303 </summary>
289 </summary>
304 <details-menu class="details-dropdown">
290 <details-menu class="details-dropdown">
305
291
306 <div class="dropdown-item">
292 <div class="dropdown-item">
307 <span>${final_path}</span>
293 <span>${final_path}</span>
308 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
309 </div>
295 </div>
310
296
311 <div class="dropdown-divider"></div>
297 <div class="dropdown-divider"></div>
312
298
313 <div class="dropdown-item">
299 <div class="dropdown-item">
314 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
315 <a href="${permalink}">¶ permalink</a>
301 <a href="${permalink}">¶ permalink</a>
316 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
317 </div>
303 </div>
318
304
319
305
320 </details-menu>
306 </details-menu>
321 </details>
307 </details>
322
308
323 </span>
309 </span>
324
310
325 ${diff_ops(final_file_name, filediff)}
311 ${diff_ops(final_file_name, filediff)}
326
312
327 </label>
313 </label>
328
314
329 ${diff_menu(filediff, use_comments=use_comments)}
315 ${diff_menu(filediff, use_comments=use_comments)}
330 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
331
317
332 ## new/deleted/empty content case
318 ## new/deleted/empty content case
333 % if not filediff.hunks:
319 % if not filediff.hunks:
334 ## Comment container, on "fakes" hunk that contains all data to render comments
320 ## Comment container, on "fakes" hunk that contains all data to render comments
335 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
336 % endif
322 % endif
337
323
338 %if filediff.limited_diff:
324 %if filediff.limited_diff:
339 <tr class="cb-warning cb-collapser">
325 <tr class="cb-warning cb-collapser">
340 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
341 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
342 </td>
328 </td>
343 </tr>
329 </tr>
344 %else:
330 %else:
345 %if over_lines_changed_limit:
331 %if over_lines_changed_limit:
346 <tr class="cb-warning cb-collapser">
332 <tr class="cb-warning cb-collapser">
347 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
348 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
349 <a href="#" class="cb-expand"
335 <a href="#" class="cb-expand"
350 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
351 </a>
337 </a>
352 <a href="#" class="cb-collapse"
338 <a href="#" class="cb-collapse"
353 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
354 </a>
340 </a>
355 </td>
341 </td>
356 </tr>
342 </tr>
357 %endif
343 %endif
358 %endif
344 %endif
359
345
360 % for hunk in filediff.hunks:
346 % for hunk in filediff.hunks:
361 <tr class="cb-hunk">
347 <tr class="cb-hunk">
362 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
363 ## TODO: dan: add ajax loading of more context here
349 ## TODO: dan: add ajax loading of more context here
364 ## <a href="#">
350 ## <a href="#">
365 <i class="icon-more"></i>
351 <i class="icon-more"></i>
366 ## </a>
352 ## </a>
367 </td>
353 </td>
368 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
369 @@
355 @@
370 -${hunk.source_start},${hunk.source_length}
356 -${hunk.source_start},${hunk.source_length}
371 +${hunk.target_start},${hunk.target_length}
357 +${hunk.target_start},${hunk.target_length}
372 ${hunk.section_header}
358 ${hunk.section_header}
373 </td>
359 </td>
374 </tr>
360 </tr>
375
361
376 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
377 % endfor
363 % endfor
378
364
379 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
380
366
381 ## outdated comments that do not fit into currently displayed lines
367 ## outdated comments that do not fit into currently displayed lines
382 % for lineno, comments in unmatched_comments.items():
368 % for lineno, comments in unmatched_comments.items():
383
369
384 %if c.user_session_attrs["diffmode"] == 'unified':
370 %if c.user_session_attrs["diffmode"] == 'unified':
385 % if loop.index == 0:
371 % if loop.index == 0:
386 <tr class="cb-hunk">
372 <tr class="cb-hunk">
387 <td colspan="3"></td>
373 <td colspan="3"></td>
388 <td>
374 <td>
389 <div>
375 <div>
390 ${_('Unmatched/outdated inline comments below')}
376 ${_('Unmatched/outdated inline comments below')}
391 </div>
377 </div>
392 </td>
378 </td>
393 </tr>
379 </tr>
394 % endif
380 % endif
395 <tr class="cb-line">
381 <tr class="cb-line">
396 <td class="cb-data cb-context"></td>
382 <td class="cb-data cb-context"></td>
397 <td class="cb-lineno cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
398 <td class="cb-lineno cb-context"></td>
384 <td class="cb-lineno cb-context"></td>
399 <td class="cb-content cb-context">
385 <td class="cb-content cb-context">
400 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
401 </td>
387 </td>
402 </tr>
388 </tr>
403 %elif c.user_session_attrs["diffmode"] == 'sideside':
389 %elif c.user_session_attrs["diffmode"] == 'sideside':
404 % if loop.index == 0:
390 % if loop.index == 0:
405 <tr class="cb-comment-info">
391 <tr class="cb-comment-info">
406 <td colspan="2"></td>
392 <td colspan="2"></td>
407 <td class="cb-line">
393 <td class="cb-line">
408 <div>
394 <div>
409 ${_('Unmatched/outdated inline comments below')}
395 ${_('Unmatched/outdated inline comments below')}
410 </div>
396 </div>
411 </td>
397 </td>
412 <td colspan="2"></td>
398 <td colspan="2"></td>
413 <td class="cb-line">
399 <td class="cb-line">
414 <div>
400 <div>
415 ${_('Unmatched/outdated comments below')}
401 ${_('Unmatched/outdated comments below')}
416 </div>
402 </div>
417 </td>
403 </td>
418 </tr>
404 </tr>
419 % endif
405 % endif
420 <tr class="cb-line">
406 <tr class="cb-line">
421 <td class="cb-data cb-context"></td>
407 <td class="cb-data cb-context"></td>
422 <td class="cb-lineno cb-context"></td>
408 <td class="cb-lineno cb-context"></td>
423 <td class="cb-content cb-context">
409 <td class="cb-content cb-context">
424 % if lineno.startswith('o'):
410 % if lineno.startswith('o'):
425 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
426 % endif
412 % endif
427 </td>
413 </td>
428
414
429 <td class="cb-data cb-context"></td>
415 <td class="cb-data cb-context"></td>
430 <td class="cb-lineno cb-context"></td>
416 <td class="cb-lineno cb-context"></td>
431 <td class="cb-content cb-context">
417 <td class="cb-content cb-context">
432 % if lineno.startswith('n'):
418 % if lineno.startswith('n'):
433 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
434 % endif
420 % endif
435 </td>
421 </td>
436 </tr>
422 </tr>
437 %endif
423 %endif
438
424
439 % endfor
425 % endfor
440
426
441 </table>
427 </table>
442 </div>
428 </div>
443 %endfor
429 %endfor
444
430
445 ## outdated comments that are made for a file that has been deleted
431 ## outdated comments that are made for a file that has been deleted
446 % for filename, comments_dict in (deleted_files_comments or {}).items():
432 % for filename, comments_dict in (deleted_files_comments or {}).items():
447
433
448 <%
434 <%
449 display_state = 'display: none'
435 display_state = 'display: none'
450 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
451 if open_comments_in_file:
437 if open_comments_in_file:
452 display_state = ''
438 display_state = ''
453 fid = str(id(filename))
439 fid = str(id(filename))
454 %>
440 %>
455 <div class="filediffs filediff-outdated" style="${display_state}">
441 <div class="filediffs filediff-outdated" style="${display_state}">
456 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
457 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
458 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
459 <div class="filediff-collapse-indicator icon-"></div>
445 <div class="filediff-collapse-indicator icon-"></div>
460
446
461 <span class="pill">
447 <span class="pill">
462 ## file was deleted
448 ## file was deleted
463 ${filename}
449 ${filename}
464 </span>
450 </span>
465 <span class="pill-group pull-left" >
451 <span class="pill-group pull-left" >
466 ## file op, doesn't need translation
452 ## file op, doesn't need translation
467 <span class="pill" op="removed">unresolved comments</span>
453 <span class="pill" op="removed">unresolved comments</span>
468 </span>
454 </span>
469 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}"></a>
455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}"></a>
470 <span class="pill-group pull-right">
456 <span class="pill-group pull-right">
471 <span class="pill" op="deleted">
457 <span class="pill" op="deleted">
472 % if comments_dict['stats'] >0:
458 % if comments_dict['stats'] >0:
473 -${comments_dict['stats']}
459 -${comments_dict['stats']}
474 % else:
460 % else:
475 ${comments_dict['stats']}
461 ${comments_dict['stats']}
476 % endif
462 % endif
477 </span>
463 </span>
478 </span>
464 </span>
479 </label>
465 </label>
480
466
481 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
482 <tr>
468 <tr>
483 % if c.user_session_attrs["diffmode"] == 'unified':
469 % if c.user_session_attrs["diffmode"] == 'unified':
484 <td></td>
470 <td></td>
485 %endif
471 %endif
486
472
487 <td></td>
473 <td></td>
488 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
489 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
490 ${_('There are still outdated/unresolved comments attached to it.')}
476 ${_('There are still outdated/unresolved comments attached to it.')}
491 </td>
477 </td>
492 </tr>
478 </tr>
493 %if c.user_session_attrs["diffmode"] == 'unified':
479 %if c.user_session_attrs["diffmode"] == 'unified':
494 <tr class="cb-line">
480 <tr class="cb-line">
495 <td class="cb-data cb-context"></td>
481 <td class="cb-data cb-context"></td>
496 <td class="cb-lineno cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
497 <td class="cb-lineno cb-context"></td>
483 <td class="cb-lineno cb-context"></td>
498 <td class="cb-content cb-context">
484 <td class="cb-content cb-context">
499 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
500 </td>
486 </td>
501 </tr>
487 </tr>
502 %elif c.user_session_attrs["diffmode"] == 'sideside':
488 %elif c.user_session_attrs["diffmode"] == 'sideside':
503 <tr class="cb-line">
489 <tr class="cb-line">
504 <td class="cb-data cb-context"></td>
490 <td class="cb-data cb-context"></td>
505 <td class="cb-lineno cb-context"></td>
491 <td class="cb-lineno cb-context"></td>
506 <td class="cb-content cb-context"></td>
492 <td class="cb-content cb-context"></td>
507
493
508 <td class="cb-data cb-context"></td>
494 <td class="cb-data cb-context"></td>
509 <td class="cb-lineno cb-context"></td>
495 <td class="cb-lineno cb-context"></td>
510 <td class="cb-content cb-context">
496 <td class="cb-content cb-context">
511 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
512 </td>
498 </td>
513 </tr>
499 </tr>
514 %endif
500 %endif
515 </table>
501 </table>
516 </div>
502 </div>
517 </div>
503 </div>
518 % endfor
504 % endfor
519
505
520 </div>
506 </div>
521 </div>
507 </div>
522 </%def>
508 </%def>
523
509
524 <%def name="diff_ops(file_name, filediff)">
510 <%def name="diff_ops(file_name, filediff)">
525 <%
511 <%
526 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
527 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
528 %>
514 %>
529 <span class="pill">
515 <span class="pill">
530 <i class="icon-file-text"></i>
516 <i class="icon-file-text"></i>
531 ${file_name}
517 ${file_name}
532 </span>
518 </span>
533
519
534 <span class="pill-group pull-right">
520 <span class="pill-group pull-right">
535
521
536 ## ops pills
522 ## ops pills
537 %if filediff.limited_diff:
523 %if filediff.limited_diff:
538 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
539 %endif
525 %endif
540
526
541 %if NEW_FILENODE in filediff.patch['stats']['ops']:
527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
542 <span class="pill" op="created">created</span>
528 <span class="pill" op="created">created</span>
543 %if filediff['target_mode'].startswith('120'):
529 %if filediff['target_mode'].startswith('120'):
544 <span class="pill" op="symlink">symlink</span>
530 <span class="pill" op="symlink">symlink</span>
545 %else:
531 %else:
546 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
547 %endif
533 %endif
548 %endif
534 %endif
549
535
550 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
551 <span class="pill" op="renamed">renamed</span>
537 <span class="pill" op="renamed">renamed</span>
552 %endif
538 %endif
553
539
554 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
555 <span class="pill" op="copied">copied</span>
541 <span class="pill" op="copied">copied</span>
556 %endif
542 %endif
557
543
558 %if DEL_FILENODE in filediff.patch['stats']['ops']:
544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
559 <span class="pill" op="removed">removed</span>
545 <span class="pill" op="removed">removed</span>
560 %endif
546 %endif
561
547
562 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
563 <span class="pill" op="mode">
549 <span class="pill" op="mode">
564 ${nice_mode(filediff['source_mode'])}${nice_mode(filediff['target_mode'])}
550 ${nice_mode(filediff['source_mode'])}${nice_mode(filediff['target_mode'])}
565 </span>
551 </span>
566 %endif
552 %endif
567
553
568 %if BIN_FILENODE in filediff.patch['stats']['ops']:
554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
569 <span class="pill" op="binary">binary</span>
555 <span class="pill" op="binary">binary</span>
570 %if MOD_FILENODE in filediff.patch['stats']['ops']:
556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
571 <span class="pill" op="modified">modified</span>
557 <span class="pill" op="modified">modified</span>
572 %endif
558 %endif
573 %endif
559 %endif
574
560
575 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
576 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
577
563
578 </span>
564 </span>
579
565
580 </%def>
566 </%def>
581
567
582 <%def name="nice_mode(filemode)">
568 <%def name="nice_mode(filemode)">
583 ${(filemode.startswith('100') and filemode[3:] or filemode)}
569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
584 </%def>
570 </%def>
585
571
586 <%def name="diff_menu(filediff, use_comments=False)">
572 <%def name="diff_menu(filediff, use_comments=False)">
587 <div class="filediff-menu">
573 <div class="filediff-menu">
588
574
589 %if filediff.diffset.source_ref:
575 %if filediff.diffset.source_ref:
590
576
591 ## FILE BEFORE CHANGES
577 ## FILE BEFORE CHANGES
592 %if filediff.operation in ['D', 'M']:
578 %if filediff.operation in ['D', 'M']:
593 <a
579 <a
594 class="tooltip"
580 class="tooltip"
595 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
596 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
597 >
583 >
598 ${_('Show file before')}
584 ${_('Show file before')}
599 </a> |
585 </a> |
600 %else:
586 %else:
601 <span
587 <span
602 class="tooltip"
588 class="tooltip"
603 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
604 >
590 >
605 ${_('Show file before')}
591 ${_('Show file before')}
606 </span> |
592 </span> |
607 %endif
593 %endif
608
594
609 ## FILE AFTER CHANGES
595 ## FILE AFTER CHANGES
610 %if filediff.operation in ['A', 'M']:
596 %if filediff.operation in ['A', 'M']:
611 <a
597 <a
612 class="tooltip"
598 class="tooltip"
613 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
614 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
615 >
601 >
616 ${_('Show file after')}
602 ${_('Show file after')}
617 </a>
603 </a>
618 %else:
604 %else:
619 <span
605 <span
620 class="tooltip"
606 class="tooltip"
621 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
622 >
608 >
623 ${_('Show file after')}
609 ${_('Show file after')}
624 </span>
610 </span>
625 %endif
611 %endif
626
612
627 % if use_comments:
613 % if use_comments:
628 |
614 |
629 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
630 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
616 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
631 </a>
619 </a>
632 % endif
620 % endif
633
621
634 %endif
622 %endif
635
623
636 </div>
624 </div>
637 </%def>
625 </%def>
638
626
639
627
640 <%def name="inline_comments_container(comments, active_pattern_entries=None)">
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
641
629
642 <div class="inline-comments">
630 <div class="inline-comments">
643 %for comment in comments:
631 %for comment in comments:
644 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
645 %endfor
633 %endfor
646 % if comments and comments[-1].outdated:
634
647 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
635 <%
648 ${_('Add another comment')}
636 extra_class = ''
649 </span>
637 extra_style = ''
650 % else:
638
651 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
639 if comments and comments[-1].outdated:
652 ${_('Add another comment')}
640 extra_class = ' comment-outdated'
653 </span>
641 extra_style = 'display: none;'
654 % endif
655
642
643 %>
644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
645 <div class="reply-thread-container${extra_class}">
646 <div class="reply-thread-gravatar">
647 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
656 </div>
648 </div>
649 <div class="reply-thread-reply-button">
650 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
651 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
652 </div>
653 <div class="reply-thread-last"></div>
654 </div>
655 </div>
656 </div>
657
657 </%def>
658 </%def>
658
659
659 <%!
660 <%!
660
661
661 def get_inline_comments(comments, filename):
662 def get_inline_comments(comments, filename):
662 if hasattr(filename, 'unicode_path'):
663 if hasattr(filename, 'unicode_path'):
663 filename = filename.unicode_path
664 filename = filename.unicode_path
664
665
665 if not isinstance(filename, (unicode, str)):
666 if not isinstance(filename, (unicode, str)):
666 return None
667 return None
667
668
668 if comments and filename in comments:
669 if comments and filename in comments:
669 return comments[filename]
670 return comments[filename]
670
671
671 return None
672 return None
672
673
673 def get_comments_for(diff_type, comments, filename, line_version, line_number):
674 def get_comments_for(diff_type, comments, filename, line_version, line_number):
674 if hasattr(filename, 'unicode_path'):
675 if hasattr(filename, 'unicode_path'):
675 filename = filename.unicode_path
676 filename = filename.unicode_path
676
677
677 if not isinstance(filename, (unicode, str)):
678 if not isinstance(filename, (unicode, str)):
678 return None
679 return None
679
680
680 file_comments = get_inline_comments(comments, filename)
681 file_comments = get_inline_comments(comments, filename)
681 if file_comments is None:
682 if file_comments is None:
682 return None
683 return None
683
684
684 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
685 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
685 if line_key in file_comments:
686 if line_key in file_comments:
686 data = file_comments.pop(line_key)
687 data = file_comments.pop(line_key)
687 return data
688 return data
688 %>
689 %>
689
690
690 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
691 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
691
692
692 <% chunk_count = 1 %>
693 <% chunk_count = 1 %>
693 %for loop_obj, item in h.looper(hunk.sideside):
694 %for loop_obj, item in h.looper(hunk.sideside):
694 <%
695 <%
695 line = item
696 line = item
696 i = loop_obj.index
697 i = loop_obj.index
697 prev_line = loop_obj.previous
698 prev_line = loop_obj.previous
698 old_line_anchor, new_line_anchor = None, None
699 old_line_anchor, new_line_anchor = None, None
699
700
700 if line.original.lineno:
701 if line.original.lineno:
701 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
702 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
702 if line.modified.lineno:
703 if line.modified.lineno:
703 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
704 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
704
705
705 line_action = line.modified.action or line.original.action
706 line_action = line.modified.action or line.original.action
706 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
707 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
707 %>
708 %>
708
709
709 <tr class="cb-line">
710 <tr class="cb-line">
710 <td class="cb-data ${action_class(line.original.action)}"
711 <td class="cb-data ${action_class(line.original.action)}"
711 data-line-no="${line.original.lineno}"
712 data-line-no="${line.original.lineno}"
712 >
713 >
713
714
714 <% line_old_comments, line_old_comments_no_drafts = None, None %>
715 <% line_old_comments, line_old_comments_no_drafts = None, None %>
715 %if line.original.get_comment_args:
716 %if line.original.get_comment_args:
716 <%
717 <%
717 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
718 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
718 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
719 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
719 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
720 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
720 %>
721 %>
721 %endif
722 %endif
722 %if line_old_comments_no_drafts:
723 %if line_old_comments_no_drafts:
723 % if has_outdated:
724 % if has_outdated:
724 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
725 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
725 % else:
726 % else:
726 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
727 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
727 % endif
728 % endif
728 %endif
729 %endif
729 </td>
730 </td>
730 <td class="cb-lineno ${action_class(line.original.action)}"
731 <td class="cb-lineno ${action_class(line.original.action)}"
731 data-line-no="${line.original.lineno}"
732 data-line-no="${line.original.lineno}"
732 %if old_line_anchor:
733 %if old_line_anchor:
733 id="${old_line_anchor}"
734 id="${old_line_anchor}"
734 %endif
735 %endif
735 >
736 >
736 %if line.original.lineno:
737 %if line.original.lineno:
737 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
738 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
738 %endif
739 %endif
739 </td>
740 </td>
741
742 <% line_no = 'o{}'.format(line.original.lineno) %>
740 <td class="cb-content ${action_class(line.original.action)}"
743 <td class="cb-content ${action_class(line.original.action)}"
741 data-line-no="o${line.original.lineno}"
744 data-line-no="${line_no}"
742 >
745 >
743 %if use_comments and line.original.lineno:
746 %if use_comments and line.original.lineno:
744 ${render_add_comment_button()}
747 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
745 %endif
748 %endif
746 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
749 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
747
750
748 %if use_comments and line.original.lineno and line_old_comments:
751 %if use_comments and line.original.lineno and line_old_comments:
749 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries)}
752 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
750 %endif
753 %endif
751
754
752 </td>
755 </td>
753 <td class="cb-data ${action_class(line.modified.action)}"
756 <td class="cb-data ${action_class(line.modified.action)}"
754 data-line-no="${line.modified.lineno}"
757 data-line-no="${line.modified.lineno}"
755 >
758 >
756 <div>
759 <div>
757
760
758 <% line_new_comments, line_new_comments_no_drafts = None, None %>
761 <% line_new_comments, line_new_comments_no_drafts = None, None %>
759 %if line.modified.get_comment_args:
762 %if line.modified.get_comment_args:
760 <%
763 <%
761 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
764 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
762 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
765 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
763 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
766 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
764 %>
767 %>
765 %endif
768 %endif
766
769
767 %if line_new_comments_no_drafts:
770 %if line_new_comments_no_drafts:
768 % if has_outdated:
771 % if has_outdated:
769 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
772 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
770 % else:
773 % else:
771 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
774 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
772 % endif
775 % endif
773 %endif
776 %endif
774 </div>
777 </div>
775 </td>
778 </td>
776 <td class="cb-lineno ${action_class(line.modified.action)}"
779 <td class="cb-lineno ${action_class(line.modified.action)}"
777 data-line-no="${line.modified.lineno}"
780 data-line-no="${line.modified.lineno}"
778 %if new_line_anchor:
781 %if new_line_anchor:
779 id="${new_line_anchor}"
782 id="${new_line_anchor}"
780 %endif
783 %endif
781 >
784 >
782 %if line.modified.lineno:
785 %if line.modified.lineno:
783 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
786 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
784 %endif
787 %endif
785 </td>
788 </td>
789
790 <% line_no = 'n{}'.format(line.modified.lineno) %>
786 <td class="cb-content ${action_class(line.modified.action)}"
791 <td class="cb-content ${action_class(line.modified.action)}"
787 data-line-no="n${line.modified.lineno}"
792 data-line-no="${line_no}"
788 >
793 >
789 %if use_comments and line.modified.lineno:
794 %if use_comments and line.modified.lineno:
790 ${render_add_comment_button()}
795 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
791 %endif
796 %endif
792 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
797 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
793 %if use_comments and line.modified.lineno and line_new_comments:
794 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
795 %endif
796 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
798 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
797 <div class="nav-chunk" style="visibility: hidden">
799 <div class="nav-chunk" style="visibility: hidden">
798 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
800 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
799 </div>
801 </div>
800 <% chunk_count +=1 %>
802 <% chunk_count +=1 %>
801 % endif
803 % endif
804 %if use_comments and line.modified.lineno and line_new_comments:
805 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
806 %endif
807
802 </td>
808 </td>
803 </tr>
809 </tr>
804 %endfor
810 %endfor
805 </%def>
811 </%def>
806
812
807
813
808 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
814 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
809 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
815 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
810
816
811 <%
817 <%
812 old_line_anchor, new_line_anchor = None, None
818 old_line_anchor, new_line_anchor = None, None
813 if old_line_no:
819 if old_line_no:
814 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
820 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
815 if new_line_no:
821 if new_line_no:
816 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
822 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
817 %>
823 %>
818 <tr class="cb-line">
824 <tr class="cb-line">
819 <td class="cb-data ${action_class(action)}">
825 <td class="cb-data ${action_class(action)}">
820 <div>
826 <div>
821
827
822 <% comments, comments_no_drafts = None, None %>
828 <% comments, comments_no_drafts = None, None %>
823 %if comments_args:
829 %if comments_args:
824 <%
830 <%
825 comments = get_comments_for('unified', inline_comments, *comments_args)
831 comments = get_comments_for('unified', inline_comments, *comments_args)
826 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
832 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
827 has_outdated = any([x.outdated for x in comments_no_drafts])
833 has_outdated = any([x.outdated for x in comments_no_drafts])
828 %>
834 %>
829 %endif
835 %endif
830
836
831 % if comments_no_drafts:
837 % if comments_no_drafts:
832 % if has_outdated:
838 % if has_outdated:
833 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
839 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
834 % else:
840 % else:
835 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
841 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
836 % endif
842 % endif
837 % endif
843 % endif
838 </div>
844 </div>
839 </td>
845 </td>
840 <td class="cb-lineno ${action_class(action)}"
846 <td class="cb-lineno ${action_class(action)}"
841 data-line-no="${old_line_no}"
847 data-line-no="${old_line_no}"
842 %if old_line_anchor:
848 %if old_line_anchor:
843 id="${old_line_anchor}"
849 id="${old_line_anchor}"
844 %endif
850 %endif
845 >
851 >
846 %if old_line_anchor:
852 %if old_line_anchor:
847 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
853 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
848 %endif
854 %endif
849 </td>
855 </td>
850 <td class="cb-lineno ${action_class(action)}"
856 <td class="cb-lineno ${action_class(action)}"
851 data-line-no="${new_line_no}"
857 data-line-no="${new_line_no}"
852 %if new_line_anchor:
858 %if new_line_anchor:
853 id="${new_line_anchor}"
859 id="${new_line_anchor}"
854 %endif
860 %endif
855 >
861 >
856 %if new_line_anchor:
862 %if new_line_anchor:
857 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
863 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
858 %endif
864 %endif
859 </td>
865 </td>
866 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
860 <td class="cb-content ${action_class(action)}"
867 <td class="cb-content ${action_class(action)}"
861 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
868 data-line-no="${line_no}"
862 >
869 >
863 %if use_comments:
870 %if use_comments:
864 ${render_add_comment_button()}
871 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
865 %endif
872 %endif
866 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
873 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
867 %if use_comments and comments:
874 %if use_comments and comments:
868 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
875 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
869 %endif
876 %endif
870 </td>
877 </td>
871 </tr>
878 </tr>
872 %endfor
879 %endfor
873 </%def>
880 </%def>
874
881
875
882
876 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
883 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
877 % if diff_mode == 'unified':
884 % if diff_mode == 'unified':
878 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
885 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
879 % elif diff_mode == 'sideside':
886 % elif diff_mode == 'sideside':
880 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
887 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
881 % else:
888 % else:
882 <tr class="cb-line">
889 <tr class="cb-line">
883 <td>unknown diff mode</td>
890 <td>unknown diff mode</td>
884 </tr>
891 </tr>
885 % endif
892 % endif
886 </%def>file changes
893 </%def>file changes
887
894
888
895
889 <%def name="render_add_comment_button()">
896 <%def name="render_add_comment_button(line_no='', f_path='')">
890 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
897 % if not c.rhodecode_user.is_default:
898 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
891 <span><i class="icon-comment"></i></span>
899 <span><i class="icon-comment"></i></span>
892 </button>
900 </button>
901 % endif
893 </%def>
902 </%def>
894
903
895 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
904 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
896 <% diffset_container_id = h.md5(diffset.target_ref) %>
905 <% diffset_container_id = h.md5(diffset.target_ref) %>
897
906
898 <div id="diff-file-sticky" class="diffset-menu clearinner">
907 <div id="diff-file-sticky" class="diffset-menu clearinner">
899 ## auto adjustable
908 ## auto adjustable
900 <div class="sidebar__inner">
909 <div class="sidebar__inner">
901 <div class="sidebar__bar">
910 <div class="sidebar__bar">
902 <div class="pull-right">
911 <div class="pull-right">
903 <div class="btn-group">
912 <div class="btn-group">
904 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
913 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
905 <i class="icon-wide-mode"></i>
914 <i class="icon-wide-mode"></i>
906 </a>
915 </a>
907 </div>
916 </div>
908 <div class="btn-group">
917 <div class="btn-group">
909
918
910 <a
919 <a
911 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
920 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
912 title="${h.tooltip(_('View diff as side by side'))}"
921 title="${h.tooltip(_('View diff as side by side'))}"
913 href="${h.current_route_path(request, diffmode='sideside')}">
922 href="${h.current_route_path(request, diffmode='sideside')}">
914 <span>${_('Side by Side')}</span>
923 <span>${_('Side by Side')}</span>
915 </a>
924 </a>
916
925
917 <a
926 <a
918 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
927 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
919 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
928 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
920 <span>${_('Unified')}</span>
929 <span>${_('Unified')}</span>
921 </a>
930 </a>
922
931
923 % if range_diff_on is True:
932 % if range_diff_on is True:
924 <a
933 <a
925 title="${_('Turn off: Show the diff as commit range')}"
934 title="${_('Turn off: Show the diff as commit range')}"
926 class="btn btn-primary"
935 class="btn btn-primary"
927 href="${h.current_route_path(request, **{"range-diff":"0"})}">
936 href="${h.current_route_path(request, **{"range-diff":"0"})}">
928 <span>${_('Range Diff')}</span>
937 <span>${_('Range Diff')}</span>
929 </a>
938 </a>
930 % elif range_diff_on is False:
939 % elif range_diff_on is False:
931 <a
940 <a
932 title="${_('Show the diff as commit range')}"
941 title="${_('Show the diff as commit range')}"
933 class="btn"
942 class="btn"
934 href="${h.current_route_path(request, **{"range-diff":"1"})}">
943 href="${h.current_route_path(request, **{"range-diff":"1"})}">
935 <span>${_('Range Diff')}</span>
944 <span>${_('Range Diff')}</span>
936 </a>
945 </a>
937 % endif
946 % endif
938 </div>
947 </div>
939 <div class="btn-group">
948 <div class="btn-group">
940
949
941 <div class="pull-left">
950 <div class="pull-left">
942 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
951 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
943 </div>
952 </div>
944
953
945 </div>
954 </div>
946 </div>
955 </div>
947 <div class="pull-left">
956 <div class="pull-left">
948 <div class="btn-group">
957 <div class="btn-group">
949 <div class="pull-left">
958 <div class="pull-left">
950 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
959 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
951 </div>
960 </div>
952
961
953 </div>
962 </div>
954 </div>
963 </div>
955 </div>
964 </div>
956 <div class="fpath-placeholder pull-left">
965 <div class="fpath-placeholder pull-left">
957 <i class="icon-file-text"></i>
966 <i class="icon-file-text"></i>
958 <strong class="fpath-placeholder-text">
967 <strong class="fpath-placeholder-text">
959 Context file:
968 Context file:
960 </strong>
969 </strong>
961 </div>
970 </div>
962 <div class="pull-right noselect">
971 <div class="pull-right noselect">
963
972
964 %if commit:
973 %if commit:
965 <span>
974 <span>
966 <code>${h.show_id(commit)}</code>
975 <code>${h.show_id(commit)}</code>
967 </span>
976 </span>
968 %elif pull_request_menu and pull_request_menu.get('pull_request'):
977 %elif pull_request_menu and pull_request_menu.get('pull_request'):
969 <span>
978 <span>
970 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
979 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
971 </span>
980 </span>
972 %endif
981 %endif
973 % if commit or pull_request_menu:
982 % if commit or pull_request_menu:
974 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
983 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
975 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
984 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
976 <i class="icon-angle-up"></i>
985 <i class="icon-angle-up"></i>
977 </span>
986 </span>
978 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
987 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
979 <i class="icon-angle-down"></i>
988 <i class="icon-angle-down"></i>
980 </span>
989 </span>
981 % endif
990 % endif
982 </div>
991 </div>
983 <div class="sidebar_inner_shadow"></div>
992 <div class="sidebar_inner_shadow"></div>
984 </div>
993 </div>
985 </div>
994 </div>
986
995
987 % if diffset:
996 % if diffset:
988 %if diffset.limited_diff:
997 %if diffset.limited_diff:
989 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
998 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
990 %else:
999 %else:
991 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1000 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
992 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1001 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
993
1002
994 %endif
1003 %endif
995 ## case on range-diff placeholder needs to be updated
1004 ## case on range-diff placeholder needs to be updated
996 % if range_diff_on is True:
1005 % if range_diff_on is True:
997 <% file_placeholder = _('Disabled on range diff') %>
1006 <% file_placeholder = _('Disabled on range diff') %>
998 % endif
1007 % endif
999
1008
1000 <script type="text/javascript">
1009 <script type="text/javascript">
1001 var feedFilesOptions = function (query, initialData) {
1010 var feedFilesOptions = function (query, initialData) {
1002 var data = {results: []};
1011 var data = {results: []};
1003 var isQuery = typeof query.term !== 'undefined';
1012 var isQuery = typeof query.term !== 'undefined';
1004
1013
1005 var section = _gettext('Changed files');
1014 var section = _gettext('Changed files');
1006 var filteredData = [];
1015 var filteredData = [];
1007
1016
1008 //filter results
1017 //filter results
1009 $.each(initialData.results, function (idx, value) {
1018 $.each(initialData.results, function (idx, value) {
1010
1019
1011 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1020 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1012 filteredData.push({
1021 filteredData.push({
1013 'id': this.id,
1022 'id': this.id,
1014 'text': this.text,
1023 'text': this.text,
1015 "ops": this.ops,
1024 "ops": this.ops,
1016 })
1025 })
1017 }
1026 }
1018
1027
1019 });
1028 });
1020
1029
1021 data.results = filteredData;
1030 data.results = filteredData;
1022
1031
1023 query.callback(data);
1032 query.callback(data);
1024 };
1033 };
1025
1034
1026 var selectionFormatter = function(data, escapeMarkup) {
1035 var selectionFormatter = function(data, escapeMarkup) {
1027 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1036 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1028 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1037 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1029 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1038 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1030 '<span class="pill" op="added">{0}</span>' +
1039 '<span class="pill" op="added">{0}</span>' +
1031 '<span class="pill" op="deleted">{1}</span>' +
1040 '<span class="pill" op="deleted">{1}</span>' +
1032 '</div>'
1041 '</div>'
1033 ;
1042 ;
1034 var added = data['ops']['added'];
1043 var added = data['ops']['added'];
1035 if (added === 0) {
1044 if (added === 0) {
1036 // don't show +0
1045 // don't show +0
1037 added = 0;
1046 added = 0;
1038 } else {
1047 } else {
1039 added = '+' + added;
1048 added = '+' + added;
1040 }
1049 }
1041
1050
1042 var deleted = -1*data['ops']['deleted'];
1051 var deleted = -1*data['ops']['deleted'];
1043
1052
1044 tmpl += pill.format(added, deleted);
1053 tmpl += pill.format(added, deleted);
1045 return container.format(tmpl);
1054 return container.format(tmpl);
1046 };
1055 };
1047 var formatFileResult = function(result, container, query, escapeMarkup) {
1056 var formatFileResult = function(result, container, query, escapeMarkup) {
1048 return selectionFormatter(result, escapeMarkup);
1057 return selectionFormatter(result, escapeMarkup);
1049 };
1058 };
1050
1059
1051 var formatSelection = function (data, container) {
1060 var formatSelection = function (data, container) {
1052 return '${file_placeholder}'
1061 return '${file_placeholder}'
1053 };
1062 };
1054
1063
1055 if (window.preloadFileFilterData === undefined) {
1064 if (window.preloadFileFilterData === undefined) {
1056 window.preloadFileFilterData = {}
1065 window.preloadFileFilterData = {}
1057 }
1066 }
1058
1067
1059 preloadFileFilterData["${diffset_container_id}"] = {
1068 preloadFileFilterData["${diffset_container_id}"] = {
1060 results: [
1069 results: [
1061 % for filediff in diffset.files:
1070 % for filediff in diffset.files:
1062 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1071 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1063 text:"${filediff.patch['filename']}",
1072 text:"${filediff.patch['filename']}",
1064 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1073 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1065 % endfor
1074 % endfor
1066 ]
1075 ]
1067 };
1076 };
1068
1077
1069 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1078 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1070 var diffFileFilter = $(diffFileFilterId).select2({
1079 var diffFileFilter = $(diffFileFilterId).select2({
1071 'dropdownAutoWidth': true,
1080 'dropdownAutoWidth': true,
1072 'width': 'auto',
1081 'width': 'auto',
1073
1082
1074 containerCssClass: "drop-menu",
1083 containerCssClass: "drop-menu",
1075 dropdownCssClass: "drop-menu-dropdown",
1084 dropdownCssClass: "drop-menu-dropdown",
1076 data: preloadFileFilterData["${diffset_container_id}"],
1085 data: preloadFileFilterData["${diffset_container_id}"],
1077 query: function(query) {
1086 query: function(query) {
1078 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1087 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1079 },
1088 },
1080 initSelection: function(element, callback) {
1089 initSelection: function(element, callback) {
1081 callback({'init': true});
1090 callback({'init': true});
1082 },
1091 },
1083 formatResult: formatFileResult,
1092 formatResult: formatFileResult,
1084 formatSelection: formatSelection
1093 formatSelection: formatSelection
1085 });
1094 });
1086
1095
1087 % if range_diff_on is True:
1096 % if range_diff_on is True:
1088 diffFileFilter.select2("enable", false);
1097 diffFileFilter.select2("enable", false);
1089 % endif
1098 % endif
1090
1099
1091 $(diffFileFilterId).on('select2-selecting', function (e) {
1100 $(diffFileFilterId).on('select2-selecting', function (e) {
1092 var idSelector = e.choice.id;
1101 var idSelector = e.choice.id;
1093
1102
1094 // expand the container if we quick-select the field
1103 // expand the container if we quick-select the field
1095 $('#'+idSelector).next().prop('checked', false);
1104 $('#'+idSelector).next().prop('checked', false);
1096 // hide the mast as we later do preventDefault()
1105 // hide the mast as we later do preventDefault()
1097 $("#select2-drop-mask").click();
1106 $("#select2-drop-mask").click();
1098
1107
1099 window.location.hash = '#'+idSelector;
1108 window.location.hash = '#'+idSelector;
1100 updateSticky();
1109 updateSticky();
1101
1110
1102 e.preventDefault();
1111 e.preventDefault();
1103 });
1112 });
1104
1113
1105 diffNavText = 'diff navigation:'
1114 diffNavText = 'diff navigation:'
1106
1115
1107 getCurrentChunk = function () {
1116 getCurrentChunk = function () {
1108
1117
1109 var chunksAll = $('.nav-chunk').filter(function () {
1118 var chunksAll = $('.nav-chunk').filter(function () {
1110 return $(this).parents('.filediff').prev().get(0).checked !== true
1119 return $(this).parents('.filediff').prev().get(0).checked !== true
1111 })
1120 })
1112 var chunkSelected = $('.nav-chunk.selected');
1121 var chunkSelected = $('.nav-chunk.selected');
1113 var initial = false;
1122 var initial = false;
1114
1123
1115 if (chunkSelected.length === 0) {
1124 if (chunkSelected.length === 0) {
1116 // no initial chunk selected, we pick first
1125 // no initial chunk selected, we pick first
1117 chunkSelected = $(chunksAll.get(0));
1126 chunkSelected = $(chunksAll.get(0));
1118 var initial = true;
1127 var initial = true;
1119 }
1128 }
1120
1129
1121 return {
1130 return {
1122 'all': chunksAll,
1131 'all': chunksAll,
1123 'selected': chunkSelected,
1132 'selected': chunkSelected,
1124 'initial': initial,
1133 'initial': initial,
1125 }
1134 }
1126 }
1135 }
1127
1136
1128 animateDiffNavText = function () {
1137 animateDiffNavText = function () {
1129 var $diffNav = $('#diff_nav')
1138 var $diffNav = $('#diff_nav')
1130
1139
1131 var callback = function () {
1140 var callback = function () {
1132 $diffNav.animate({'opacity': 1.00}, 200)
1141 $diffNav.animate({'opacity': 1.00}, 200)
1133 };
1142 };
1134 $diffNav.animate({'opacity': 0.15}, 200, callback);
1143 $diffNav.animate({'opacity': 0.15}, 200, callback);
1135 }
1144 }
1136
1145
1137 scrollToChunk = function (moveBy) {
1146 scrollToChunk = function (moveBy) {
1138 var chunk = getCurrentChunk();
1147 var chunk = getCurrentChunk();
1139 var all = chunk.all
1148 var all = chunk.all
1140 var selected = chunk.selected
1149 var selected = chunk.selected
1141
1150
1142 var curPos = all.index(selected);
1151 var curPos = all.index(selected);
1143 var newPos = curPos;
1152 var newPos = curPos;
1144 if (!chunk.initial) {
1153 if (!chunk.initial) {
1145 var newPos = curPos + moveBy;
1154 var newPos = curPos + moveBy;
1146 }
1155 }
1147
1156
1148 var curElem = all.get(newPos);
1157 var curElem = all.get(newPos);
1149
1158
1150 if (curElem === undefined) {
1159 if (curElem === undefined) {
1151 // end or back
1160 // end or back
1152 $('#diff_nav').html('no next diff element:')
1161 $('#diff_nav').html('no next diff element:')
1153 animateDiffNavText()
1162 animateDiffNavText()
1154 return
1163 return
1155 } else if (newPos < 0) {
1164 } else if (newPos < 0) {
1156 $('#diff_nav').html('no previous diff element:')
1165 $('#diff_nav').html('no previous diff element:')
1157 animateDiffNavText()
1166 animateDiffNavText()
1158 return
1167 return
1159 } else {
1168 } else {
1160 $('#diff_nav').html(diffNavText)
1169 $('#diff_nav').html(diffNavText)
1161 }
1170 }
1162
1171
1163 curElem = $(curElem)
1172 curElem = $(curElem)
1164 var offset = 100;
1173 var offset = 100;
1165 $(window).scrollTop(curElem.position().top - offset);
1174 $(window).scrollTop(curElem.position().top - offset);
1166
1175
1167 //clear selection
1176 //clear selection
1168 all.removeClass('selected')
1177 all.removeClass('selected')
1169 curElem.addClass('selected')
1178 curElem.addClass('selected')
1170 }
1179 }
1171
1180
1172 scrollToPrevChunk = function () {
1181 scrollToPrevChunk = function () {
1173 scrollToChunk(-1)
1182 scrollToChunk(-1)
1174 }
1183 }
1175 scrollToNextChunk = function () {
1184 scrollToNextChunk = function () {
1176 scrollToChunk(1)
1185 scrollToChunk(1)
1177 }
1186 }
1178
1187
1179 </script>
1188 </script>
1180 % endif
1189 % endif
1181
1190
1182 <script type="text/javascript">
1191 <script type="text/javascript">
1183 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1192 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1184
1193
1185 $(document).ready(function () {
1194 $(document).ready(function () {
1186
1195
1187 var contextPrefix = _gettext('Context file: ');
1196 var contextPrefix = _gettext('Context file: ');
1188 ## sticky sidebar
1197 ## sticky sidebar
1189 var sidebarElement = document.getElementById('diff-file-sticky');
1198 var sidebarElement = document.getElementById('diff-file-sticky');
1190 sidebar = new StickySidebar(sidebarElement, {
1199 sidebar = new StickySidebar(sidebarElement, {
1191 topSpacing: 0,
1200 topSpacing: 0,
1192 bottomSpacing: 0,
1201 bottomSpacing: 0,
1193 innerWrapperSelector: '.sidebar__inner'
1202 innerWrapperSelector: '.sidebar__inner'
1194 });
1203 });
1195 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1204 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1196 // reset our file so it's not holding new value
1205 // reset our file so it's not holding new value
1197 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1206 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1198 });
1207 });
1199
1208
1200 updateSticky = function () {
1209 updateSticky = function () {
1201 sidebar.updateSticky();
1210 sidebar.updateSticky();
1202 Waypoint.refreshAll();
1211 Waypoint.refreshAll();
1203 };
1212 };
1204
1213
1205 var animateText = function (fPath, anchorId) {
1214 var animateText = function (fPath, anchorId) {
1206 fPath = Select2.util.escapeMarkup(fPath);
1215 fPath = Select2.util.escapeMarkup(fPath);
1207 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1216 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1208 };
1217 };
1209
1218
1210 ## dynamic file waypoints
1219 ## dynamic file waypoints
1211 var setFPathInfo = function(fPath, anchorId){
1220 var setFPathInfo = function(fPath, anchorId){
1212 animateText(fPath, anchorId)
1221 animateText(fPath, anchorId)
1213 };
1222 };
1214
1223
1215 var codeBlock = $('.filediff');
1224 var codeBlock = $('.filediff');
1216
1225
1217 // forward waypoint
1226 // forward waypoint
1218 codeBlock.waypoint(
1227 codeBlock.waypoint(
1219 function(direction) {
1228 function(direction) {
1220 if (direction === "down"){
1229 if (direction === "down"){
1221 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1230 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1222 }
1231 }
1223 }, {
1232 }, {
1224 offset: function () {
1233 offset: function () {
1225 return 70;
1234 return 70;
1226 },
1235 },
1227 context: '.fpath-placeholder'
1236 context: '.fpath-placeholder'
1228 }
1237 }
1229 );
1238 );
1230
1239
1231 // backward waypoint
1240 // backward waypoint
1232 codeBlock.waypoint(
1241 codeBlock.waypoint(
1233 function(direction) {
1242 function(direction) {
1234 if (direction === "up"){
1243 if (direction === "up"){
1235 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1244 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1236 }
1245 }
1237 }, {
1246 }, {
1238 offset: function () {
1247 offset: function () {
1239 return -this.element.clientHeight + 90;
1248 return -this.element.clientHeight + 90;
1240 },
1249 },
1241 context: '.fpath-placeholder'
1250 context: '.fpath-placeholder'
1242 }
1251 }
1243 );
1252 );
1244
1253
1245 toggleWideDiff = function (el) {
1254 toggleWideDiff = function (el) {
1246 updateSticky();
1255 updateSticky();
1247 var wide = Rhodecode.comments.toggleWideMode(this);
1256 var wide = Rhodecode.comments.toggleWideMode(this);
1248 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1257 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1249 if (wide === true) {
1258 if (wide === true) {
1250 $(el).addClass('btn-active');
1259 $(el).addClass('btn-active');
1251 } else {
1260 } else {
1252 $(el).removeClass('btn-active');
1261 $(el).removeClass('btn-active');
1253 }
1262 }
1254 return null;
1263 return null;
1255 };
1264 };
1256
1265
1257 var preloadDiffMenuData = {
1266 var preloadDiffMenuData = {
1258 results: [
1267 results: [
1259
1268
1260 ## Whitespace change
1269 ## Whitespace change
1261 % if request.GET.get('ignorews', '') == '1':
1270 % if request.GET.get('ignorews', '') == '1':
1262 {
1271 {
1263 id: 2,
1272 id: 2,
1264 text: _gettext('Show whitespace changes'),
1273 text: _gettext('Show whitespace changes'),
1265 action: function () {},
1274 action: function () {},
1266 url: "${h.current_route_path(request, ignorews=0)|n}"
1275 url: "${h.current_route_path(request, ignorews=0)|n}"
1267 },
1276 },
1268 % else:
1277 % else:
1269 {
1278 {
1270 id: 2,
1279 id: 2,
1271 text: _gettext('Hide whitespace changes'),
1280 text: _gettext('Hide whitespace changes'),
1272 action: function () {},
1281 action: function () {},
1273 url: "${h.current_route_path(request, ignorews=1)|n}"
1282 url: "${h.current_route_path(request, ignorews=1)|n}"
1274 },
1283 },
1275 % endif
1284 % endif
1276
1285
1277 ## FULL CONTEXT
1286 ## FULL CONTEXT
1278 % if request.GET.get('fullcontext', '') == '1':
1287 % if request.GET.get('fullcontext', '') == '1':
1279 {
1288 {
1280 id: 3,
1289 id: 3,
1281 text: _gettext('Hide full context diff'),
1290 text: _gettext('Hide full context diff'),
1282 action: function () {},
1291 action: function () {},
1283 url: "${h.current_route_path(request, fullcontext=0)|n}"
1292 url: "${h.current_route_path(request, fullcontext=0)|n}"
1284 },
1293 },
1285 % else:
1294 % else:
1286 {
1295 {
1287 id: 3,
1296 id: 3,
1288 text: _gettext('Show full context diff'),
1297 text: _gettext('Show full context diff'),
1289 action: function () {},
1298 action: function () {},
1290 url: "${h.current_route_path(request, fullcontext=1)|n}"
1299 url: "${h.current_route_path(request, fullcontext=1)|n}"
1291 },
1300 },
1292 % endif
1301 % endif
1293
1302
1294 ]
1303 ]
1295 };
1304 };
1296
1305
1297 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1306 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1298 $(diffMenuId).select2({
1307 $(diffMenuId).select2({
1299 minimumResultsForSearch: -1,
1308 minimumResultsForSearch: -1,
1300 containerCssClass: "drop-menu-no-width",
1309 containerCssClass: "drop-menu-no-width",
1301 dropdownCssClass: "drop-menu-dropdown",
1310 dropdownCssClass: "drop-menu-dropdown",
1302 dropdownAutoWidth: true,
1311 dropdownAutoWidth: true,
1303 data: preloadDiffMenuData,
1312 data: preloadDiffMenuData,
1304 placeholder: "${_('...')}",
1313 placeholder: "${_('...')}",
1305 });
1314 });
1306 $(diffMenuId).on('select2-selecting', function (e) {
1315 $(diffMenuId).on('select2-selecting', function (e) {
1307 e.choice.action();
1316 e.choice.action();
1308 if (e.choice.url !== null) {
1317 if (e.choice.url !== null) {
1309 window.location = e.choice.url
1318 window.location = e.choice.url
1310 }
1319 }
1311 });
1320 });
1312 toggleExpand = function (el, diffsetEl) {
1321 toggleExpand = function (el, diffsetEl) {
1313 var el = $(el);
1322 var el = $(el);
1314 if (el.hasClass('collapsed')) {
1323 if (el.hasClass('collapsed')) {
1315 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1324 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1316 el.removeClass('collapsed');
1325 el.removeClass('collapsed');
1317 el.html(
1326 el.html(
1318 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1327 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1319 _gettext('Collapse all files'));
1328 _gettext('Collapse all files'));
1320 }
1329 }
1321 else {
1330 else {
1322 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1331 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1323 el.addClass('collapsed');
1332 el.addClass('collapsed');
1324 el.html(
1333 el.html(
1325 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1334 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1326 _gettext('Expand all files'));
1335 _gettext('Expand all files'));
1327 }
1336 }
1328 updateSticky()
1337 updateSticky()
1329 };
1338 };
1330
1339
1331 toggleCommitExpand = function (el) {
1340 toggleCommitExpand = function (el) {
1332 var $el = $(el);
1341 var $el = $(el);
1333 var commits = $el.data('toggleCommitsCnt');
1342 var commits = $el.data('toggleCommitsCnt');
1334 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1343 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1335 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1344 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1336
1345
1337 if ($el.hasClass('collapsed')) {
1346 if ($el.hasClass('collapsed')) {
1338 $('.compare_select').show();
1347 $('.compare_select').show();
1339 $('.compare_select_hidden').hide();
1348 $('.compare_select_hidden').hide();
1340
1349
1341 $el.removeClass('collapsed');
1350 $el.removeClass('collapsed');
1342 $el.html(
1351 $el.html(
1343 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1352 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1344 collapseMsg);
1353 collapseMsg);
1345 }
1354 }
1346 else {
1355 else {
1347 $('.compare_select').hide();
1356 $('.compare_select').hide();
1348 $('.compare_select_hidden').show();
1357 $('.compare_select_hidden').show();
1349 $el.addClass('collapsed');
1358 $el.addClass('collapsed');
1350 $el.html(
1359 $el.html(
1351 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1360 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1352 expandMsg);
1361 expandMsg);
1353 }
1362 }
1354 updateSticky();
1363 updateSticky();
1355 };
1364 };
1356
1365
1357 // get stored diff mode and pre-enable it
1366 // get stored diff mode and pre-enable it
1358 if (templateContext.session_attrs.wide_diff_mode === "true") {
1367 if (templateContext.session_attrs.wide_diff_mode === "true") {
1359 Rhodecode.comments.toggleWideMode(null);
1368 Rhodecode.comments.toggleWideMode(null);
1360 $('.toggle-wide-diff').addClass('btn-active');
1369 $('.toggle-wide-diff').addClass('btn-active');
1361 updateSticky();
1370 updateSticky();
1362 }
1371 }
1363
1372
1364 // DIFF NAV //
1373 // DIFF NAV //
1365
1374
1366 // element to detect scroll direction of
1375 // element to detect scroll direction of
1367 var $window = $(window);
1376 var $window = $(window);
1368
1377
1369 // initialize last scroll position
1378 // initialize last scroll position
1370 var lastScrollY = $window.scrollTop();
1379 var lastScrollY = $window.scrollTop();
1371
1380
1372 $window.on('resize scrollstop', {latency: 350}, function () {
1381 $window.on('resize scrollstop', {latency: 350}, function () {
1373 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1382 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1374
1383
1375 // get current scroll position
1384 // get current scroll position
1376 var currentScrollY = $window.scrollTop();
1385 var currentScrollY = $window.scrollTop();
1377
1386
1378 // determine current scroll direction
1387 // determine current scroll direction
1379 if (currentScrollY > lastScrollY) {
1388 if (currentScrollY > lastScrollY) {
1380 var y = 'down'
1389 var y = 'down'
1381 } else if (currentScrollY !== lastScrollY) {
1390 } else if (currentScrollY !== lastScrollY) {
1382 var y = 'up';
1391 var y = 'up';
1383 }
1392 }
1384
1393
1385 var pos = -1; // by default we use last element in viewport
1394 var pos = -1; // by default we use last element in viewport
1386 if (y === 'down') {
1395 if (y === 'down') {
1387 pos = -1;
1396 pos = -1;
1388 } else if (y === 'up') {
1397 } else if (y === 'up') {
1389 pos = 0;
1398 pos = 0;
1390 }
1399 }
1391
1400
1392 if (visibleChunks.length > 0) {
1401 if (visibleChunks.length > 0) {
1393 $('.nav-chunk').removeClass('selected');
1402 $('.nav-chunk').removeClass('selected');
1394 $(visibleChunks.get(pos)).addClass('selected');
1403 $(visibleChunks.get(pos)).addClass('selected');
1395 }
1404 }
1396
1405
1397 // update last scroll position to current position
1406 // update last scroll position to current position
1398 lastScrollY = currentScrollY;
1407 lastScrollY = currentScrollY;
1399
1408
1400 });
1409 });
1401 $('#diff_nav').html(diffNavText);
1410 $('#diff_nav').html(diffNavText);
1402
1411
1403 });
1412 });
1404 </script>
1413 </script>
1405
1414
1406 </%def>
1415 </%def>
@@ -1,489 +1,489 b''
1 ## DATA TABLE RE USABLE ELEMENTS
1 ## DATA TABLE RE USABLE ELEMENTS
2 ## usage:
2 ## usage:
3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
4 <%namespace name="base" file="/base/base.mako"/>
5
5
6 <%def name="metatags_help()">
6 <%def name="metatags_help()">
7 <table>
7 <table>
8 <%
8 <%
9 example_tags = [
9 example_tags = [
10 ('state','[stable]'),
10 ('state','[stable]'),
11 ('state','[stale]'),
11 ('state','[stale]'),
12 ('state','[featured]'),
12 ('state','[featured]'),
13 ('state','[dev]'),
13 ('state','[dev]'),
14 ('state','[dead]'),
14 ('state','[dead]'),
15 ('state','[deprecated]'),
15 ('state','[deprecated]'),
16
16
17 ('label','[personal]'),
17 ('label','[personal]'),
18 ('generic','[v2.0.0]'),
18 ('generic','[v2.0.0]'),
19
19
20 ('lang','[lang =&gt; JavaScript]'),
20 ('lang','[lang =&gt; JavaScript]'),
21 ('license','[license =&gt; LicenseName]'),
21 ('license','[license =&gt; LicenseName]'),
22
22
23 ('ref','[requires =&gt; RepoName]'),
23 ('ref','[requires =&gt; RepoName]'),
24 ('ref','[recommends =&gt; GroupName]'),
24 ('ref','[recommends =&gt; GroupName]'),
25 ('ref','[conflicts =&gt; SomeName]'),
25 ('ref','[conflicts =&gt; SomeName]'),
26 ('ref','[base =&gt; SomeName]'),
26 ('ref','[base =&gt; SomeName]'),
27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
28 ('see','[see =&gt; http://rhodecode.com]'),
28 ('see','[see =&gt; http://rhodecode.com]'),
29 ]
29 ]
30 %>
30 %>
31 % for tag_type, tag in example_tags:
31 % for tag_type, tag in example_tags:
32 <tr>
32 <tr>
33 <td>${tag|n}</td>
33 <td>${tag|n}</td>
34 <td>${h.style_metatag(tag_type, tag)|n}</td>
34 <td>${h.style_metatag(tag_type, tag)|n}</td>
35 </tr>
35 </tr>
36 % endfor
36 % endfor
37 </table>
37 </table>
38 </%def>
38 </%def>
39
39
40 <%def name="render_description(description, stylify_metatags)">
40 <%def name="render_description(description, stylify_metatags)">
41 <%
41 <%
42 tags = []
42 tags = []
43 if stylify_metatags:
43 if stylify_metatags:
44 tags, description = h.extract_metatags(description)
44 tags, description = h.extract_metatags(description)
45 %>
45 %>
46 % for tag_type, tag in tags:
46 % for tag_type, tag in tags:
47 ${h.style_metatag(tag_type, tag)|n,trim}
47 ${h.style_metatag(tag_type, tag)|n,trim}
48 % endfor
48 % endfor
49 <code style="white-space: pre-wrap">${description}</code>
49 <code style="white-space: pre-wrap">${description}</code>
50 </%def>
50 </%def>
51
51
52 ## REPOSITORY RENDERERS
52 ## REPOSITORY RENDERERS
53 <%def name="quick_menu(repo_name)">
53 <%def name="quick_menu(repo_name)">
54 <i class="icon-more"></i>
54 <i class="icon-more"></i>
55 <div class="menu_items_container hidden">
55 <div class="menu_items_container hidden">
56 <ul class="menu_items">
56 <ul class="menu_items">
57 <li>
57 <li>
58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
59 <span>${_('Summary')}</span>
59 <span>${_('Summary')}</span>
60 </a>
60 </a>
61 </li>
61 </li>
62 <li>
62 <li>
63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
64 <span>${_('Commits')}</span>
64 <span>${_('Commits')}</span>
65 </a>
65 </a>
66 </li>
66 </li>
67 <li>
67 <li>
68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
69 <span>${_('Files')}</span>
69 <span>${_('Files')}</span>
70 </a>
70 </a>
71 </li>
71 </li>
72 <li>
72 <li>
73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
74 <span>${_('Fork')}</span>
74 <span>${_('Fork')}</span>
75 </a>
75 </a>
76 </li>
76 </li>
77 </ul>
77 </ul>
78 </div>
78 </div>
79 </%def>
79 </%def>
80
80
81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_of,short_name=False,admin=False)">
82 <%
82 <%
83 def get_name(name,short_name=short_name):
83 def get_name(name,short_name=short_name):
84 if short_name:
84 if short_name:
85 return name.split('/')[-1]
85 return name.split('/')[-1]
86 else:
86 else:
87 return name
87 return name
88 %>
88 %>
89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
90 ##NAME
90 ##NAME
91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
92
92
93 ##TYPE OF REPO
93 ##TYPE OF REPO
94 %if h.is_hg(rtype):
94 %if h.is_hg(rtype):
95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
96 %elif h.is_git(rtype):
96 %elif h.is_git(rtype):
97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
98 %elif h.is_svn(rtype):
98 %elif h.is_svn(rtype):
99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
100 %endif
100 %endif
101
101
102 ##PRIVATE/PUBLIC
102 ##PRIVATE/PUBLIC
103 %if private is True and c.visual.show_private_icon:
103 %if private is True and c.visual.show_private_icon:
104 <i class="icon-lock" title="${_('Private repository')}"></i>
104 <i class="icon-lock" title="${_('Private repository')}"></i>
105 %elif private is False and c.visual.show_public_icon:
105 %elif private is False and c.visual.show_public_icon:
106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
107 %else:
107 %else:
108 <span></span>
108 <span></span>
109 %endif
109 %endif
110 ${get_name(name)}
110 ${get_name(name)}
111 </a>
111 </a>
112 %if fork_of:
112 %if fork_of:
113 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
113 <a href="${h.route_path('repo_summary',repo_name=fork_of.repo_name)}"><i class="icon-code-fork"></i></a>
114 %endif
114 %endif
115 %if rstate == 'repo_state_pending':
115 %if rstate == 'repo_state_pending':
116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
117 (${_('creating...')})
117 (${_('creating...')})
118 </span>
118 </span>
119 %endif
119 %endif
120
120
121 </div>
121 </div>
122 </%def>
122 </%def>
123
123
124 <%def name="repo_desc(description, stylify_metatags)">
124 <%def name="repo_desc(description, stylify_metatags)">
125 <%
125 <%
126 tags, description = h.extract_metatags(description)
126 tags, description = h.extract_metatags(description)
127 %>
127 %>
128
128
129 <div class="truncate-wrap">
129 <div class="truncate-wrap">
130 % if stylify_metatags:
130 % if stylify_metatags:
131 % for tag_type, tag in tags:
131 % for tag_type, tag in tags:
132 ${h.style_metatag(tag_type, tag)|n}
132 ${h.style_metatag(tag_type, tag)|n}
133 % endfor
133 % endfor
134 % endif
134 % endif
135 ${description}
135 ${description}
136 </div>
136 </div>
137
137
138 </%def>
138 </%def>
139
139
140 <%def name="last_change(last_change)">
140 <%def name="last_change(last_change)">
141 ${h.age_component(last_change, time_is_local=True)}
141 ${h.age_component(last_change, time_is_local=True)}
142 </%def>
142 </%def>
143
143
144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
145 <div>
145 <div>
146 %if rev >= 0:
146 %if rev >= 0:
147 <code><a class="tooltip-hovercard" data-hovercard-alt=${h.tooltip(last_msg)} data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
147 <code><a class="tooltip-hovercard" data-hovercard-alt=${h.tooltip(last_msg)} data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
148 %else:
148 %else:
149 ${_('No commits yet')}
149 ${_('No commits yet')}
150 %endif
150 %endif
151 </div>
151 </div>
152 </%def>
152 </%def>
153
153
154 <%def name="rss(name)">
154 <%def name="rss(name)">
155 %if c.rhodecode_user.username != h.DEFAULT_USER:
155 %if c.rhodecode_user.username != h.DEFAULT_USER:
156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
157 %else:
157 %else:
158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
159 %endif
159 %endif
160 </%def>
160 </%def>
161
161
162 <%def name="atom(name)">
162 <%def name="atom(name)">
163 %if c.rhodecode_user.username != h.DEFAULT_USER:
163 %if c.rhodecode_user.username != h.DEFAULT_USER:
164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
165 %else:
165 %else:
166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
167 %endif
167 %endif
168 </%def>
168 </%def>
169
169
170 <%def name="repo_actions(repo_name, super_user=True)">
170 <%def name="repo_actions(repo_name, super_user=True)">
171 <div>
171 <div>
172 <div class="grid_edit">
172 <div class="grid_edit">
173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
174 Edit
174 Edit
175 </a>
175 </a>
176 </div>
176 </div>
177 <div class="grid_delete">
177 <div class="grid_delete">
178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
179 <input class="btn btn-link btn-danger" id="remove_${repo_name}" name="remove_${repo_name}"
179 <input class="btn btn-link btn-danger" id="remove_${repo_name}" name="remove_${repo_name}"
180 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${repo_name}')"
180 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${repo_name}')"
181 type="submit" value="Delete"
181 type="submit" value="Delete"
182 >
182 >
183 ${h.end_form()}
183 ${h.end_form()}
184 </div>
184 </div>
185 </div>
185 </div>
186 </%def>
186 </%def>
187
187
188 <%def name="repo_state(repo_state)">
188 <%def name="repo_state(repo_state)">
189 <div>
189 <div>
190 %if repo_state == 'repo_state_pending':
190 %if repo_state == 'repo_state_pending':
191 <div class="tag tag4">${_('Creating')}</div>
191 <div class="tag tag4">${_('Creating')}</div>
192 %elif repo_state == 'repo_state_created':
192 %elif repo_state == 'repo_state_created':
193 <div class="tag tag1">${_('Created')}</div>
193 <div class="tag tag1">${_('Created')}</div>
194 %else:
194 %else:
195 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
195 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
196 %endif
196 %endif
197 </div>
197 </div>
198 </%def>
198 </%def>
199
199
200
200
201 ## REPO GROUP RENDERERS
201 ## REPO GROUP RENDERERS
202 <%def name="quick_repo_group_menu(repo_group_name)">
202 <%def name="quick_repo_group_menu(repo_group_name)">
203 <i class="icon-more"></i>
203 <i class="icon-more"></i>
204 <div class="menu_items_container hidden">
204 <div class="menu_items_container hidden">
205 <ul class="menu_items">
205 <ul class="menu_items">
206 <li>
206 <li>
207 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
207 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
208 </li>
208 </li>
209
209
210 </ul>
210 </ul>
211 </div>
211 </div>
212 </%def>
212 </%def>
213
213
214 <%def name="repo_group_name(repo_group_name, children_groups=None)">
214 <%def name="repo_group_name(repo_group_name, children_groups=None)">
215 <div>
215 <div>
216 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
216 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
217 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
217 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
218 %if children_groups:
218 %if children_groups:
219 ${h.literal(' &raquo; '.join(children_groups))}
219 ${h.literal(' &raquo; '.join(children_groups))}
220 %else:
220 %else:
221 ${repo_group_name}
221 ${repo_group_name}
222 %endif
222 %endif
223 </a>
223 </a>
224 </div>
224 </div>
225 </%def>
225 </%def>
226
226
227 <%def name="repo_group_desc(description, personal, stylify_metatags)">
227 <%def name="repo_group_desc(description, personal, stylify_metatags)">
228
228
229 <%
229 <%
230 if stylify_metatags:
230 if stylify_metatags:
231 tags, description = h.extract_metatags(description)
231 tags, description = h.extract_metatags(description)
232 %>
232 %>
233
233
234 <div class="truncate-wrap">
234 <div class="truncate-wrap">
235 % if personal:
235 % if personal:
236 <div class="metatag" tag="personal">${_('personal')}</div>
236 <div class="metatag" tag="personal">${_('personal')}</div>
237 % endif
237 % endif
238
238
239 % if stylify_metatags:
239 % if stylify_metatags:
240 % for tag_type, tag in tags:
240 % for tag_type, tag in tags:
241 ${h.style_metatag(tag_type, tag)|n}
241 ${h.style_metatag(tag_type, tag)|n}
242 % endfor
242 % endfor
243 % endif
243 % endif
244 ${description}
244 ${description}
245 </div>
245 </div>
246
246
247 </%def>
247 </%def>
248
248
249 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
249 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
250 <div class="grid_edit">
250 <div class="grid_edit">
251 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
251 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
252 </div>
252 </div>
253 <div class="grid_delete">
253 <div class="grid_delete">
254 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
254 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
255 <input class="btn btn-link btn-danger" id="remove_${repo_group_name}" name="remove_${repo_group_name}"
255 <input class="btn btn-link btn-danger" id="remove_${repo_group_name}" name="remove_${repo_group_name}"
256 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository group'), _gettext('Delete'), '${_ungettext('`{}` with {} repository','`{}` with {} repositories',gr_count).format(repo_group_name, gr_count)}')"
256 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository group'), _gettext('Delete'), '${_ungettext('`{}` with {} repository','`{}` with {} repositories',gr_count).format(repo_group_name, gr_count)}')"
257 type="submit" value="Delete"
257 type="submit" value="Delete"
258 >
258 >
259 ${h.end_form()}
259 ${h.end_form()}
260 </div>
260 </div>
261 </%def>
261 </%def>
262
262
263
263
264 <%def name="user_actions(user_id, username)">
264 <%def name="user_actions(user_id, username)">
265 <div class="grid_edit">
265 <div class="grid_edit">
266 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
266 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
267 ${_('Edit')}
267 ${_('Edit')}
268 </a>
268 </a>
269 </div>
269 </div>
270 <div class="grid_delete">
270 <div class="grid_delete">
271 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
271 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
272 <input class="btn btn-link btn-danger" id="remove_user_${user_id}" name="remove_user_${user_id}"
272 <input class="btn btn-link btn-danger" id="remove_user_${user_id}" name="remove_user_${user_id}"
273 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Delete'), '${username}')"
273 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Delete'), '${username}')"
274 type="submit" value="Delete"
274 type="submit" value="Delete"
275 >
275 >
276 ${h.end_form()}
276 ${h.end_form()}
277 </div>
277 </div>
278 </%def>
278 </%def>
279
279
280 <%def name="user_group_actions(user_group_id, user_group_name)">
280 <%def name="user_group_actions(user_group_id, user_group_name)">
281 <div class="grid_edit">
281 <div class="grid_edit">
282 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
282 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
283 </div>
283 </div>
284 <div class="grid_delete">
284 <div class="grid_delete">
285 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
285 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
286 <input class="btn btn-link btn-danger" id="remove_group_${user_group_id}" name="remove_group_${user_group_id}"
286 <input class="btn btn-link btn-danger" id="remove_group_${user_group_id}" name="remove_group_${user_group_id}"
287 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user group'), _gettext('Delete'), '${user_group_name}')"
287 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user group'), _gettext('Delete'), '${user_group_name}')"
288 type="submit" value="Delete"
288 type="submit" value="Delete"
289 >
289 >
290 ${h.end_form()}
290 ${h.end_form()}
291 </div>
291 </div>
292 </%def>
292 </%def>
293
293
294
294
295 <%def name="user_name(user_id, username)">
295 <%def name="user_name(user_id, username)">
296 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
296 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
297 </%def>
297 </%def>
298
298
299 <%def name="user_profile(username)">
299 <%def name="user_profile(username)">
300 ${base.gravatar_with_user(username, 16, tooltip=True)}
300 ${base.gravatar_with_user(username, 16, tooltip=True)}
301 </%def>
301 </%def>
302
302
303 <%def name="user_group_name(user_group_name)">
303 <%def name="user_group_name(user_group_name)">
304 <div>
304 <div>
305 <i class="icon-user-group" title="${_('User group')}"></i>
305 <i class="icon-user-group" title="${_('User group')}"></i>
306 ${h.link_to_group(user_group_name)}
306 ${h.link_to_group(user_group_name)}
307 </div>
307 </div>
308 </%def>
308 </%def>
309
309
310
310
311 ## GISTS
311 ## GISTS
312
312
313 <%def name="gist_gravatar(full_contact)">
313 <%def name="gist_gravatar(full_contact)">
314 <div class="gist_gravatar">
314 <div class="gist_gravatar">
315 ${base.gravatar(full_contact, 30)}
315 ${base.gravatar(full_contact, 30)}
316 </div>
316 </div>
317 </%def>
317 </%def>
318
318
319 <%def name="gist_access_id(gist_access_id, full_contact)">
319 <%def name="gist_access_id(gist_access_id, full_contact)">
320 <div>
320 <div>
321 <code>
321 <code>
322 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">${gist_access_id}</a>
322 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">${gist_access_id}</a>
323 </code>
323 </code>
324 </div>
324 </div>
325 </%def>
325 </%def>
326
326
327 <%def name="gist_author(full_contact, created_on, expires)">
327 <%def name="gist_author(full_contact, created_on, expires)">
328 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
328 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
329 </%def>
329 </%def>
330
330
331
331
332 <%def name="gist_created(created_on)">
332 <%def name="gist_created(created_on)">
333 <div class="created">
333 <div class="created">
334 ${h.age_component(created_on, time_is_local=True)}
334 ${h.age_component(created_on, time_is_local=True)}
335 </div>
335 </div>
336 </%def>
336 </%def>
337
337
338 <%def name="gist_expires(expires)">
338 <%def name="gist_expires(expires)">
339 <div class="created">
339 <div class="created">
340 %if expires == -1:
340 %if expires == -1:
341 ${_('never')}
341 ${_('never')}
342 %else:
342 %else:
343 ${h.age_component(h.time_to_utcdatetime(expires))}
343 ${h.age_component(h.time_to_utcdatetime(expires))}
344 %endif
344 %endif
345 </div>
345 </div>
346 </%def>
346 </%def>
347
347
348 <%def name="gist_type(gist_type)">
348 <%def name="gist_type(gist_type)">
349 %if gist_type == 'public':
349 %if gist_type == 'public':
350 <span class="tag tag-gist-public disabled">${_('Public Gist')}</span>
350 <span class="tag tag-gist-public disabled">${_('Public Gist')}</span>
351 %else:
351 %else:
352 <span class="tag tag-gist-private disabled">${_('Private Gist')}</span>
352 <span class="tag tag-gist-private disabled">${_('Private Gist')}</span>
353 %endif
353 %endif
354 </%def>
354 </%def>
355
355
356 <%def name="gist_description(gist_description)">
356 <%def name="gist_description(gist_description)">
357 ${gist_description}
357 ${gist_description}
358 </%def>
358 </%def>
359
359
360
360
361 ## PULL REQUESTS GRID RENDERERS
361 ## PULL REQUESTS GRID RENDERERS
362
362
363 <%def name="pullrequest_target_repo(repo_name)">
363 <%def name="pullrequest_target_repo(repo_name)">
364 <div class="truncate">
364 <div class="truncate">
365 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
365 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
366 </div>
366 </div>
367 </%def>
367 </%def>
368
368
369 <%def name="pullrequest_status(status)">
369 <%def name="pullrequest_status(status)">
370 <i class="icon-circle review-status-${status}"></i>
370 <i class="icon-circle review-status-${status}"></i>
371 </%def>
371 </%def>
372
372
373 <%def name="pullrequest_title(title, description)">
373 <%def name="pullrequest_title(title, description)">
374 ${title}
374 ${title}
375 </%def>
375 </%def>
376
376
377 <%def name="pullrequest_comments(comments_nr)">
377 <%def name="pullrequest_comments(comments_nr)">
378 <i class="icon-comment"></i> ${comments_nr}
378 <i class="icon-comment"></i> ${comments_nr}
379 </%def>
379 </%def>
380
380
381 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
381 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
382 <code>
382 <code>
383 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
383 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
384 % if short:
384 % if short:
385 !${pull_request_id}
385 !${pull_request_id}
386 % else:
386 % else:
387 ${_('Pull request !{}').format(pull_request_id)}
387 ${_('Pull request !{}').format(pull_request_id)}
388 % endif
388 % endif
389 </a>
389 </a>
390 </code>
390 </code>
391 % if state not in ['created']:
391 % if state not in ['created']:
392 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
392 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
393 % endif
393 % endif
394
394
395 % if is_wip:
395 % if is_wip:
396 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
396 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
397 % endif
397 % endif
398 </%def>
398 </%def>
399
399
400 <%def name="pullrequest_updated_on(updated_on)">
400 <%def name="pullrequest_updated_on(updated_on)">
401 ${h.age_component(h.time_to_utcdatetime(updated_on))}
401 ${h.age_component(h.time_to_utcdatetime(updated_on))}
402 </%def>
402 </%def>
403
403
404 <%def name="pullrequest_author(full_contact)">
404 <%def name="pullrequest_author(full_contact)">
405 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
405 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
406 </%def>
406 </%def>
407
407
408
408
409 ## ARTIFACT RENDERERS
409 ## ARTIFACT RENDERERS
410 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
410 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
411 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
411 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
412 ${artifact_display_name or '_EMPTY_NAME_'}
412 ${artifact_display_name or '_EMPTY_NAME_'}
413 </a>
413 </a>
414 </%def>
414 </%def>
415
415
416 <%def name="repo_artifact_uid(repo_name, file_uid)">
416 <%def name="repo_artifact_uid(repo_name, file_uid)">
417 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
417 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
418 </%def>
418 </%def>
419
419
420 <%def name="repo_artifact_sha256(artifact_sha256)">
420 <%def name="repo_artifact_sha256(artifact_sha256)">
421 <div class="code">${h.shorter(artifact_sha256, 12)}</div>
421 <div class="code">${h.shorter(artifact_sha256, 12)}</div>
422 </%def>
422 </%def>
423
423
424 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
424 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
425 ## <div class="grid_edit">
425 ## <div class="grid_edit">
426 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
426 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
427 ## </div>
427 ## </div>
428 <div class="grid_edit">
428 <div class="grid_edit">
429 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
429 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
430 </div>
430 </div>
431 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
431 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
432 <div class="grid_delete">
432 <div class="grid_delete">
433 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
433 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
434 <input class="btn btn-link btn-danger" id="remove_artifact_${file_store_id}" name="remove_artifact_${file_store_id}"
434 <input class="btn btn-link btn-danger" id="remove_artifact_${file_store_id}" name="remove_artifact_${file_store_id}"
435 onclick="submitConfirm(event, this, _gettext('Confirm to delete this artifact'), _gettext('Delete'), '${file_uid}')"
435 onclick="submitConfirm(event, this, _gettext('Confirm to delete this artifact'), _gettext('Delete'), '${file_uid}')"
436 type="submit" value="${_('Delete')}"
436 type="submit" value="${_('Delete')}"
437 >
437 >
438 ${h.end_form()}
438 ${h.end_form()}
439 </div>
439 </div>
440 % endif
440 % endif
441 </%def>
441 </%def>
442
442
443 <%def name="markup_form(form_id, form_text='', help_text=None)">
443 <%def name="markup_form(form_id, form_text='', help_text=None)">
444
444
445 <div class="markup-form">
445 <div class="markup-form">
446 <div class="markup-form-area">
446 <div class="markup-form-area">
447 <div class="markup-form-area-header">
447 <div class="markup-form-area-header">
448 <ul class="nav-links clearfix">
448 <ul class="nav-links clearfix">
449 <li class="active">
449 <li class="active">
450 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
450 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
451 </li>
451 </li>
452 <li class="">
452 <li class="">
453 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
453 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
454 </li>
454 </li>
455 </ul>
455 </ul>
456 </div>
456 </div>
457
457
458 <div class="markup-form-area-write" style="display: block;">
458 <div class="markup-form-area-write" style="display: block;">
459 <div id="edit-container_${form_id}">
459 <div id="edit-container_${form_id}" style="margin-top: -1px">
460 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
460 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
461 </div>
461 </div>
462 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
462 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
463 <div id="preview-box_${form_id}" class="preview-box"></div>
463 <div id="preview-box_${form_id}" class="preview-box"></div>
464 </div>
464 </div>
465 </div>
465 </div>
466
466
467 <div class="markup-form-area-footer">
467 <div class="markup-form-area-footer">
468 <div class="toolbar">
468 <div class="toolbar">
469 <div class="toolbar-text">
469 <div class="toolbar-text">
470 ${(_('Parsed using %s syntax') % (
470 ${(_('Parsed using %s syntax') % (
471 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
471 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
472 )
472 )
473 )|n}
473 )|n}
474 </div>
474 </div>
475 </div>
475 </div>
476 </div>
476 </div>
477 </div>
477 </div>
478
478
479 <div class="markup-form-footer">
479 <div class="markup-form-footer">
480 % if help_text:
480 % if help_text:
481 <span class="help-block">${help_text}</span>
481 <span class="help-block">${help_text}</span>
482 % endif
482 % endif
483 </div>
483 </div>
484 </div>
484 </div>
485 <script type="text/javascript">
485 <script type="text/javascript">
486 new MarkupForm('${form_id}');
486 new MarkupForm('${form_id}');
487 </script>
487 </script>
488
488
489 </%def>
489 </%def>
@@ -1,249 +1,267 b''
1 <%text>
1 <%text>
2 <div style="display: none">
2 <div style="display: none">
3
3
4 <script>
4 <script>
5 var CG = new ColorGenerator();
5 var CG = new ColorGenerator();
6 </script>
6 </script>
7
7
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
9
9
10 <%
10 <%
11 if (size > 16) {
11 if (size > 16) {
12 var gravatar_class = 'gravatar gravatar-large';
12 var gravatar_class = 'gravatar gravatar-large';
13 } else {
13 } else {
14 var gravatar_class = 'gravatar';
14 var gravatar_class = 'gravatar';
15 }
15 }
16
16
17 if (tooltip) {
17 if (tooltip) {
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
19 }
19 }
20
20
21 var data_hovercard_alt = username;
21 var data_hovercard_alt = username;
22
22
23 %>
23 %>
24
24
25 <%
25 <%
26 if (show_disabled) {
26 if (show_disabled) {
27 var user_cls = 'user user-disabled';
27 var user_cls = 'user user-disabled';
28 } else {
28 } else {
29 var user_cls = 'user';
29 var user_cls = 'user';
30 }
30 }
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
32 %>
32 %>
33
33
34 <div class="rc-user">
34 <div class="rc-user">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
37 </div>
37 </div>
38
38
39 </script>
39 </script>
40
40
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 <%
42 <%
43 if (create) {
43 if (create) {
44 var edit_visibility = 'visible';
44 var edit_visibility = 'visible';
45 } else {
45 } else {
46 var edit_visibility = 'hidden';
46 var edit_visibility = 'hidden';
47 }
47 }
48
48
49 if (member.user_group && member.user_group.vote_rule) {
49 if (member.user_group && member.user_group.vote_rule) {
50 var reviewGroup = '<i class="icon-user-group"></i>';
50 var reviewGroup = '<i class="icon-user-group"></i>';
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 } else {
52 } else {
53 var reviewGroup = null;
53 var reviewGroup = null;
54 var reviewGroupColor = 'transparent';
54 var reviewGroupColor = 'transparent';
55 }
55 }
56 var rule_show = rule_show || false;
56 var rule_show = rule_show || false;
57
57
58 if (rule_show) {
58 if (rule_show) {
59 var rule_visibility = 'table-cell';
59 var rule_visibility = 'table-cell';
60 } else {
60 } else {
61 var rule_visibility = 'none';
61 var rule_visibility = 'none';
62 }
62 }
63
63
64 %>
64 %>
65
65
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67
67
68 <td style="width: 20px">
68 <td style="width: 20px">
69 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
69 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
70 <i class="icon-eye" style="color: #0ac878"></i>
70 <i class="icon-eye" style="color: #0ac878"></i>
71 </div>
71 </div>
72 <% if (role === 'reviewer') { %>
72 <% if (role === 'reviewer') { %>
73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
74 <i class="icon-circle review-status-<%= review_status %>"></i>
74 <i class="icon-circle review-status-<%= review_status %>"></i>
75 </div>
75 </div>
76 <% } else if (role === 'observer') { %>
76 <% } else if (role === 'observer') { %>
77 <div class="tooltip" title="Observer without voting right.">
77 <div class="tooltip" title="Observer without voting right.">
78 <i class="icon-circle-thin"></i>
78 <i class="icon-circle-thin"></i>
79 </div>
79 </div>
80 <% } %>
80 <% } %>
81 </td>
81 </td>
82
82
83 <td>
83 <td>
84 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
84 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
85 <%-
85 <%-
86 renderTemplate('gravatarWithUser', {
86 renderTemplate('gravatarWithUser', {
87 'size': 16,
87 'size': 16,
88 'show_disabled': false,
88 'show_disabled': false,
89 'tooltip': true,
89 'tooltip': true,
90 'username': member.username,
90 'username': member.username,
91 'user_id': member.user_id,
91 'user_id': member.user_id,
92 'user_link': member.user_link,
92 'user_link': member.user_link,
93 'gravatar_url': member.gravatar_link
93 'gravatar_url': member.gravatar_link
94 })
94 })
95 %>
95 %>
96 </div>
96 </div>
97 </td>
97 </td>
98
98
99 <td style="width: 10px">
99 <td style="width: 10px">
100 <% if (reviewGroup !== null) { %>
100 <% if (reviewGroup !== null) { %>
101 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
101 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
102 <%- reviewGroup %>
102 <%- reviewGroup %>
103 </span>
103 </span>
104 <% } %>
104 <% } %>
105 </td>
105 </td>
106
106
107 <% if (mandatory) { %>
107 <% if (mandatory) { %>
108 <td style="text-align: right;width: 10px;">
108 <td style="text-align: right;width: 10px;">
109 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
109 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
110 <i class="icon-lock"></i>
110 <i class="icon-lock"></i>
111 </div>
111 </div>
112 </td>
112 </td>
113
113
114 <% } else { %>
114 <% } else { %>
115 <td style="text-align: right;width: 10px;">
115 <td style="text-align: right;width: 10px;">
116 <% if (allowed_to_update) { %>
116 <% if (allowed_to_update) { %>
117 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
117 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
118 <i class="icon-remove"></i>
118 <i class="icon-remove"></i>
119 </div>
119 </div>
120 <% } %>
120 <% } %>
121 </td>
121 </td>
122 <% } %>
122 <% } %>
123
123
124 </tr>
124 </tr>
125
125
126 <tr id="reviewer_<%= member.user_id %>_rules">
126 <tr id="reviewer_<%= member.user_id %>_rules">
127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
128 <input type="hidden" name="__start__" value="reviewer:mapping">
128 <input type="hidden" name="__start__" value="reviewer:mapping">
129
129
130 <%if (member.user_group && member.user_group.vote_rule) { %>
130 <%if (member.user_group && member.user_group.vote_rule) { %>
131 <div class="reviewer_reason">
131 <div class="reviewer_reason">
132
132
133 <%if (member.user_group.vote_rule == -1) {%>
133 <%if (member.user_group.vote_rule == -1) {%>
134 - group votes required: ALL
134 - group votes required: ALL
135 <%} else {%>
135 <%} else {%>
136 - group votes required: <%= member.user_group.vote_rule %>
136 - group votes required: <%= member.user_group.vote_rule %>
137 <%}%>
137 <%}%>
138 </div>
138 </div>
139 <%} %>
139 <%} %>
140
140
141 <input type="hidden" name="__start__" value="reasons:sequence">
141 <input type="hidden" name="__start__" value="reasons:sequence">
142 <% for (var i = 0; i < reasons.length; i++) { %>
142 <% for (var i = 0; i < reasons.length; i++) { %>
143 <% var reason = reasons[i] %>
143 <% var reason = reasons[i] %>
144 <div class="reviewer_reason">- <%= reason %></div>
144 <div class="reviewer_reason">- <%= reason %></div>
145 <input type="hidden" name="reason" value="<%= reason %>">
145 <input type="hidden" name="reason" value="<%= reason %>">
146 <% } %>
146 <% } %>
147 <input type="hidden" name="__end__" value="reasons:sequence">
147 <input type="hidden" name="__end__" value="reasons:sequence">
148
148
149 <input type="hidden" name="__start__" value="rules:sequence">
149 <input type="hidden" name="__start__" value="rules:sequence">
150 <% for (var i = 0; i < member.rules.length; i++) { %>
150 <% for (var i = 0; i < member.rules.length; i++) { %>
151 <% var rule = member.rules[i] %>
151 <% var rule = member.rules[i] %>
152 <input type="hidden" name="rule_id" value="<%= rule %>">
152 <input type="hidden" name="rule_id" value="<%= rule %>">
153 <% } %>
153 <% } %>
154 <input type="hidden" name="__end__" value="rules:sequence">
154 <input type="hidden" name="__end__" value="rules:sequence">
155
155
156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
159
159
160 <input type="hidden" name="__end__" value="reviewer:mapping">
160 <input type="hidden" name="__end__" value="reviewer:mapping">
161 </td>
161 </td>
162 </tr>
162 </tr>
163
163
164 </script>
164 </script>
165
165
166 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
166 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
167
167
168 <%
168 <%
169 if (size > 16) {
169 if (size > 16) {
170 var gravatar_class = 'gravatar gravatar-large';
170 var gravatar_class = 'gravatar gravatar-large';
171 } else {
171 } else {
172 var gravatar_class = 'gravatar';
172 var gravatar_class = 'gravatar';
173 }
173 }
174
174
175 %>
175 %>
176
176
177 <%
177 <%
178 if (show_disabled) {
178 if (show_disabled) {
179 var user_cls = 'user user-disabled';
179 var user_cls = 'user user-disabled';
180 } else {
180 } else {
181 var user_cls = 'user';
181 var user_cls = 'user';
182 }
182 }
183
183
184 %>
184 %>
185
185
186 <div style='line-height: 20px'>
186 <div style='line-height: 20px'>
187 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
187 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
188 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
188 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
189 </div>
189 </div>
190
190
191 </script>
191 </script>
192
192
193
193
194 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
194 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
195
195
196 <div>
196 <div>
197 <% if (is_todo) { %>
197 <% if (is_todo) { %>
198 <% if (inline) { %>
198 <% if (inline) { %>
199 <strong>Inline</strong> TODO on line: <%= line_no %>
199 <strong>Inline</strong> TODO on line: <%= line_no %>
200 <% if (version_info) { %>
200 <% if (version_info) { %>
201 <%= version_info %>
201 <%= version_info %>
202 <% } %>
202 <% } %>
203 <br/>
203 <br/>
204 File: <code><%- file_name -%></code>
204 File: <code><%- file_name -%></code>
205 <% } else { %>
205 <% } else { %>
206 <% if (review_status) { %>
206 <% if (review_status) { %>
207 <i class="icon-circle review-status-<%= review_status %>"></i>
207 <i class="icon-circle review-status-<%= review_status %>"></i>
208 <% } %>
208 <% } %>
209 <strong>General</strong> TODO
209 <strong>General</strong> TODO
210 <% if (version_info) { %>
210 <% if (version_info) { %>
211 <%= version_info %>
211 <%= version_info %>
212 <% } %>
212 <% } %>
213 <% } %>
213 <% } %>
214 <% } else { %>
214 <% } else { %>
215 <% if (inline) { %>
215 <% if (inline) { %>
216 <strong>Inline</strong> comment on line: <%= line_no %>
216 <strong>Inline</strong> comment on line: <%= line_no %>
217 <% if (version_info) { %>
217 <% if (version_info) { %>
218 <%= version_info %>
218 <%= version_info %>
219 <% } %>
219 <% } %>
220 <br/>
220 <br/>
221 File: <code><%- file_name -%></code>
221 File: <code><%- file_name -%></code>
222 <% } else { %>
222 <% } else { %>
223 <% if (review_status) { %>
223 <% if (review_status) { %>
224 <i class="icon-circle review-status-<%= review_status %>"></i>
224 <i class="icon-circle review-status-<%= review_status %>"></i>
225 <% } %>
225 <% } %>
226 <strong>General</strong> comment
226 <strong>General</strong> comment
227 <% if (version_info) { %>
227 <% if (version_info) { %>
228 <%= version_info %>
228 <%= version_info %>
229 <% } %>
229 <% } %>
230 <% } %>
230 <% } %>
231 <% } %>
231 <% } %>
232 <br/>
232 <br/>
233 Created:
233 Created:
234 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
234 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
235
235
236 </div>
236 </div>
237
237
238 </script>
238 </script>
239
239
240 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
241
242 <div>
243 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
244 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
245 <br/>
246 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
247 <br/>
248 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
249 They are permanent until deleted, or converted to regular comments.<br/>
250 <br/>
251 <br/>
252 </div>
253
254 </script>
255
256
257
240 ##// END OF EJS Templates
258 ##// END OF EJS Templates
241 </div>
259 </div>
242
260
243
261
244 <script>
262 <script>
245 // registers the templates into global cache
263 // registers the templates into global cache
246 registerTemplates();
264 registerTemplates();
247 </script>
265 </script>
248
266
249 </%text>
267 </%text>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now