##// END OF EJS Templates
diffs: exclude +/- from diffs generation and make then a non-selectable markers visible only via CSS.
dan -
r3135:5cc59605 default
parent child Browse files
Show More
@@ -1,318 +1,305 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.lib.helpers import _shorten_commit_id
23 from rhodecode.lib.helpers import _shorten_commit_id
24
24
25
25
26 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
27 import urllib
27 import urllib
28
28
29 base_url = {
29 base_url = {
30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 }[name].format(**kwargs)
38 }[name].format(**kwargs)
39
39
40 if params:
40 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 return base_url
42 return base_url
43
43
44
44
45 @pytest.mark.usefixtures("app")
45 @pytest.mark.usefixtures("app")
46 class TestRepoCommitView(object):
46 class TestRepoCommitView(object):
47
47
48 def test_show_commit(self, backend):
48 def test_show_commit(self, backend):
49 commit_id = self.commit_id[backend.alias]
49 commit_id = self.commit_id[backend.alias]
50 response = self.app.get(route_path(
50 response = self.app.get(route_path(
51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 response.mustcontain('Added a symlink')
52 response.mustcontain('Added a symlink')
53 response.mustcontain(commit_id)
53 response.mustcontain(commit_id)
54 response.mustcontain('No newline at end of file')
54 response.mustcontain('No newline at end of file')
55
55
56 def test_show_raw(self, backend):
56 def test_show_raw(self, backend):
57 commit_id = self.commit_id[backend.alias]
57 commit_id = self.commit_id[backend.alias]
58 response = self.app.get(route_path(
58 response = self.app.get(route_path(
59 'repo_commit_raw',
59 'repo_commit_raw',
60 repo_name=backend.repo_name, commit_id=commit_id))
60 repo_name=backend.repo_name, commit_id=commit_id))
61 assert response.body == self.diffs[backend.alias]
61 assert response.body == self.diffs[backend.alias]
62
62
63 def test_show_raw_patch(self, backend):
63 def test_show_raw_patch(self, backend):
64 response = self.app.get(route_path(
64 response = self.app.get(route_path(
65 'repo_commit_patch', repo_name=backend.repo_name,
65 'repo_commit_patch', repo_name=backend.repo_name,
66 commit_id=self.commit_id[backend.alias]))
66 commit_id=self.commit_id[backend.alias]))
67 assert response.body == self.patches[backend.alias]
67 assert response.body == self.patches[backend.alias]
68
68
69 def test_commit_download(self, backend):
69 def test_commit_download(self, backend):
70 response = self.app.get(route_path(
70 response = self.app.get(route_path(
71 'repo_commit_download',
71 'repo_commit_download',
72 repo_name=backend.repo_name,
72 repo_name=backend.repo_name,
73 commit_id=self.commit_id[backend.alias]))
73 commit_id=self.commit_id[backend.alias]))
74 assert response.body == self.diffs[backend.alias]
74 assert response.body == self.diffs[backend.alias]
75
75
76 def test_single_commit_page_different_ops(self, backend):
76 def test_single_commit_page_different_ops(self, backend):
77 commit_id = {
77 commit_id = {
78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 'svn': '337',
80 'svn': '337',
81 }
81 }
82 commit_id = commit_id[backend.alias]
82 commit_id = commit_id[backend.alias]
83 response = self.app.get(route_path(
83 response = self.app.get(route_path(
84 'repo_commit',
84 'repo_commit',
85 repo_name=backend.repo_name, commit_id=commit_id))
85 repo_name=backend.repo_name, commit_id=commit_id))
86
86
87 response.mustcontain(_shorten_commit_id(commit_id))
87 response.mustcontain(_shorten_commit_id(commit_id))
88 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
88 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
89
89
90 # files op files
90 # files op files
91 response.mustcontain('File no longer present at commit: %s' %
91 response.mustcontain('File no longer present at commit: %s' %
92 _shorten_commit_id(commit_id))
92 _shorten_commit_id(commit_id))
93
93
94 # svn uses a different filename
94 # svn uses a different filename
95 if backend.alias == 'svn':
95 if backend.alias == 'svn':
96 response.mustcontain('new file 10644')
96 response.mustcontain('new file 10644')
97 else:
97 else:
98 response.mustcontain('new file 100644')
98 response.mustcontain('new file 100644')
99 response.mustcontain('Changed theme to ADC theme') # commit msg
99 response.mustcontain('Changed theme to ADC theme') # commit msg
100
100
101 self._check_new_diff_menus(response, right_menu=True)
101 self._check_new_diff_menus(response, right_menu=True)
102
102
103 def test_commit_range_page_different_ops(self, backend):
103 def test_commit_range_page_different_ops(self, backend):
104 commit_id_range = {
104 commit_id_range = {
105 'hg': (
105 'hg': (
106 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
106 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
107 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
107 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
108 'git': (
108 'git': (
109 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
109 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
110 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
110 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
111 'svn': (
111 'svn': (
112 '335',
112 '335',
113 '337'),
113 '337'),
114 }
114 }
115 commit_ids = commit_id_range[backend.alias]
115 commit_ids = commit_id_range[backend.alias]
116 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
116 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
117 response = self.app.get(route_path(
117 response = self.app.get(route_path(
118 'repo_commit',
118 'repo_commit',
119 repo_name=backend.repo_name, commit_id=commit_id))
119 repo_name=backend.repo_name, commit_id=commit_id))
120
120
121 response.mustcontain(_shorten_commit_id(commit_ids[0]))
121 response.mustcontain(_shorten_commit_id(commit_ids[0]))
122 response.mustcontain(_shorten_commit_id(commit_ids[1]))
122 response.mustcontain(_shorten_commit_id(commit_ids[1]))
123
123
124 # svn is special
124 # svn is special
125 if backend.alias == 'svn':
125 if backend.alias == 'svn':
126 response.mustcontain('new file 10644')
126 response.mustcontain('new file 10644')
127 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
127 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
128 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
128 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
129 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
129 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
130 else:
130 else:
131 response.mustcontain('new file 100644')
131 response.mustcontain('new file 100644')
132 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
132 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
133 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
133 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
134
134
135 # files op files
135 # files op files
136 response.mustcontain('File no longer present at commit: %s' %
136 response.mustcontain('File no longer present at commit: %s' %
137 _shorten_commit_id(commit_ids[1]))
137 _shorten_commit_id(commit_ids[1]))
138 response.mustcontain('Added docstrings to vcs.cli') # commit msg
138 response.mustcontain('Added docstrings to vcs.cli') # commit msg
139 response.mustcontain('Changed theme to ADC theme') # commit msg
139 response.mustcontain('Changed theme to ADC theme') # commit msg
140
140
141 self._check_new_diff_menus(response)
141 self._check_new_diff_menus(response)
142
142
143 def test_combined_compare_commit_page_different_ops(self, backend):
143 def test_combined_compare_commit_page_different_ops(self, backend):
144 commit_id_range = {
144 commit_id_range = {
145 'hg': (
145 'hg': (
146 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
146 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
147 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
147 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
148 'git': (
148 'git': (
149 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
149 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
150 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
150 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
151 'svn': (
151 'svn': (
152 '335',
152 '335',
153 '337'),
153 '337'),
154 }
154 }
155 commit_ids = commit_id_range[backend.alias]
155 commit_ids = commit_id_range[backend.alias]
156 response = self.app.get(route_path(
156 response = self.app.get(route_path(
157 'repo_compare',
157 'repo_compare',
158 repo_name=backend.repo_name,
158 repo_name=backend.repo_name,
159 source_ref_type='rev', source_ref=commit_ids[0],
159 source_ref_type='rev', source_ref=commit_ids[0],
160 target_ref_type='rev', target_ref=commit_ids[1], ))
160 target_ref_type='rev', target_ref=commit_ids[1], ))
161
161
162 response.mustcontain(_shorten_commit_id(commit_ids[0]))
162 response.mustcontain(_shorten_commit_id(commit_ids[0]))
163 response.mustcontain(_shorten_commit_id(commit_ids[1]))
163 response.mustcontain(_shorten_commit_id(commit_ids[1]))
164
164
165 # files op files
165 # files op files
166 response.mustcontain('File no longer present at commit: %s' %
166 response.mustcontain('File no longer present at commit: %s' %
167 _shorten_commit_id(commit_ids[1]))
167 _shorten_commit_id(commit_ids[1]))
168
168
169 # svn is special
169 # svn is special
170 if backend.alias == 'svn':
170 if backend.alias == 'svn':
171 response.mustcontain('new file 10644')
171 response.mustcontain('new file 10644')
172 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
172 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
173 else:
173 else:
174 response.mustcontain('new file 100644')
174 response.mustcontain('new file 100644')
175 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
175 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
176
176
177 response.mustcontain('Added docstrings to vcs.cli') # commit msg
177 response.mustcontain('Added docstrings to vcs.cli') # commit msg
178 response.mustcontain('Changed theme to ADC theme') # commit msg
178 response.mustcontain('Changed theme to ADC theme') # commit msg
179
179
180 self._check_new_diff_menus(response)
180 self._check_new_diff_menus(response)
181
181
182 def test_changeset_range(self, backend):
182 def test_changeset_range(self, backend):
183 self._check_changeset_range(
183 self._check_changeset_range(
184 backend, self.commit_id_range, self.commit_id_range_result)
184 backend, self.commit_id_range, self.commit_id_range_result)
185
185
186 def test_changeset_range_with_initial_commit(self, backend):
186 def test_changeset_range_with_initial_commit(self, backend):
187 commit_id_range = {
187 commit_id_range = {
188 'hg': (
188 'hg': (
189 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
189 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
190 '...6cba7170863a2411822803fa77a0a264f1310b35'),
190 '...6cba7170863a2411822803fa77a0a264f1310b35'),
191 'git': (
191 'git': (
192 'c1214f7e79e02fc37156ff215cd71275450cffc3'
192 'c1214f7e79e02fc37156ff215cd71275450cffc3'
193 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
193 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
194 'svn': '1...3',
194 'svn': '1...3',
195 }
195 }
196 commit_id_range_result = {
196 commit_id_range_result = {
197 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
197 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
198 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
198 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
199 'svn': ['1', '2', '3'],
199 'svn': ['1', '2', '3'],
200 }
200 }
201 self._check_changeset_range(
201 self._check_changeset_range(
202 backend, commit_id_range, commit_id_range_result)
202 backend, commit_id_range, commit_id_range_result)
203
203
204 def _check_changeset_range(
204 def _check_changeset_range(
205 self, backend, commit_id_ranges, commit_id_range_result):
205 self, backend, commit_id_ranges, commit_id_range_result):
206 response = self.app.get(
206 response = self.app.get(
207 route_path('repo_commit',
207 route_path('repo_commit',
208 repo_name=backend.repo_name,
208 repo_name=backend.repo_name,
209 commit_id=commit_id_ranges[backend.alias]))
209 commit_id=commit_id_ranges[backend.alias]))
210
210
211 expected_result = commit_id_range_result[backend.alias]
211 expected_result = commit_id_range_result[backend.alias]
212 response.mustcontain('{} commits'.format(len(expected_result)))
212 response.mustcontain('{} commits'.format(len(expected_result)))
213 for commit_id in expected_result:
213 for commit_id in expected_result:
214 response.mustcontain(commit_id)
214 response.mustcontain(commit_id)
215
215
216 commit_id = {
216 commit_id = {
217 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
217 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
218 'svn': '393',
218 'svn': '393',
219 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
219 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
220 }
220 }
221
221
222 commit_id_range = {
222 commit_id_range = {
223 'hg': (
223 'hg': (
224 'a53d9201d4bc278910d416d94941b7ea007ecd52'
224 'a53d9201d4bc278910d416d94941b7ea007ecd52'
225 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
225 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
226 'git': (
226 'git': (
227 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
227 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
228 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
228 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
229 'svn': '391...393',
229 'svn': '391...393',
230 }
230 }
231
231
232 commit_id_range_result = {
232 commit_id_range_result = {
233 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
233 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
234 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
234 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
235 'svn': ['391', '392', '393'],
235 'svn': ['391', '392', '393'],
236 }
236 }
237
237
238 diffs = {
238 diffs = {
239 'hg': r"""diff --git a/README b/README
239 'hg': r"""diff --git a/README b/README
240 new file mode 120000
240 new file mode 120000
241 --- /dev/null
241 --- /dev/null
242 +++ b/README
242 +++ b/README
243 @@ -0,0 +1,1 @@
243 @@ -0,0 +1,1 @@
244 +README.rst
244 +README.rst
245 \ No newline at end of file
245 \ No newline at end of file
246 """,
246 """,
247 'git': r"""diff --git a/README b/README
247 'git': r"""diff --git a/README b/README
248 new file mode 120000
248 new file mode 120000
249 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
249 index 0000000000000000000000000000000000000000..92cacd285355271487b7e379dba6ca60f9a554a4
250 --- /dev/null
250 --- /dev/null
251 +++ b/README
251 +++ b/README
252 @@ -0,0 +1 @@
252 @@ -0,0 +1 @@
253 +README.rst
253 +README.rst
254 \ No newline at end of file
254 \ No newline at end of file
255 """,
255 """,
256 'svn': """Index: README
256 'svn': """Index: README
257 ===================================================================
257 ===================================================================
258 diff --git a/README b/README
258 diff --git a/README b/README
259 new file mode 10644
259 new file mode 10644
260 --- /dev/null\t(revision 0)
260 --- /dev/null\t(revision 0)
261 +++ b/README\t(revision 393)
261 +++ b/README\t(revision 393)
262 @@ -0,0 +1 @@
262 @@ -0,0 +1 @@
263 +link README.rst
263 +link README.rst
264 \\ No newline at end of file
264 \\ No newline at end of file
265 """,
265 """,
266 }
266 }
267
267
268 patches = {
268 patches = {
269 'hg': r"""# HG changeset patch
269 'hg': r"""# HG changeset patch
270 # User Marcin Kuzminski <marcin@python-works.com>
270 # User Marcin Kuzminski <marcin@python-works.com>
271 # Date 2014-01-07 12:21:40
271 # Date 2014-01-07 12:21:40
272 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
272 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
273 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
273 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
274
274
275 Added a symlink
275 Added a symlink
276
276
277 """ + diffs['hg'],
277 """ + diffs['hg'],
278 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
278 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
279 From: Marcin Kuzminski <marcin@python-works.com>
279 From: Marcin Kuzminski <marcin@python-works.com>
280 Date: 2014-01-07 12:22:20
280 Date: 2014-01-07 12:22:20
281 Subject: [PATCH] Added a symlink
281 Subject: [PATCH] Added a symlink
282
282
283 ---
283 ---
284
284
285 """ + diffs['git'],
285 """ + diffs['git'],
286 'svn': r"""# SVN changeset patch
286 'svn': r"""# SVN changeset patch
287 # User marcin
287 # User marcin
288 # Date 2014-09-02 12:25:22.071142
288 # Date 2014-09-02 12:25:22.071142
289 # Revision 393
289 # Revision 393
290
290
291 Added a symlink
291 Added a symlink
292
292
293 """ + diffs['svn'],
293 """ + diffs['svn'],
294 }
294 }
295
295
296 def _check_diff_menus(self, response, right_menu=False,):
296 def _check_new_diff_menus(self, response, right_menu=False,):
297 # diff menus
297 # individual file diff menus
298 for elem in ['Show File', 'Unified Diff', 'Side-by-side Diff',
298 for elem in ['Show file before', 'Show file after']:
299 'Raw Diff', 'Download Diff']:
300 response.mustcontain(elem)
299 response.mustcontain(elem)
301
300
302 # right pane diff menus
301 # right pane diff menus
303 if right_menu:
302 if right_menu:
304 for elem in ['Ignore whitespace', 'Increase context',
303 for elem in ['Hide whitespace changes', 'Toggle Wide Mode diff',
305 'Hide comments']:
304 'Show full context diff']:
306 response.mustcontain(elem)
305 response.mustcontain(elem)
307
308 def _check_new_diff_menus(self, response, right_menu=False,):
309 # diff menus
310 for elem in ['Show file before', 'Show file after',
311 'Raw diff', 'Download diff']:
312 response.mustcontain(elem)
313
314 # right pane diff menus
315 if right_menu:
316 for elem in ['Ignore whitespace', 'Increase context',
317 'Hide comments']:
318 response.mustcontain(elem)
@@ -1,1237 +1,1237 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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
29
30 import collections
30 import collections
31 import difflib
31 import difflib
32 import logging
32 import logging
33 import cPickle as pickle
33 import cPickle as pickle
34 from itertools import tee, imap
34 from itertools import tee, imap
35
35
36 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.exceptions import VCSError
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
37 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42 # define max context, a file with more than this numbers of lines is unusable
42 # define max context, a file with more than this numbers of lines is unusable
43 # in browser anyway
43 # in browser anyway
44 MAX_CONTEXT = 20 * 1024
44 MAX_CONTEXT = 20 * 1024
45 DEFAULT_CONTEXT = 3
45 DEFAULT_CONTEXT = 3
46
46
47
47
48 def get_diff_context(request):
48 def get_diff_context(request):
49 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
49 return MAX_CONTEXT if request.GET.get('fullcontext', '') == '1' else DEFAULT_CONTEXT
50
50
51
51
52 def get_diff_whitespace_flag(request):
52 def get_diff_whitespace_flag(request):
53 return request.GET.get('ignorews', '') == '1'
53 return request.GET.get('ignorews', '') == '1'
54
54
55
55
56 class OPS(object):
56 class OPS(object):
57 ADD = 'A'
57 ADD = 'A'
58 MOD = 'M'
58 MOD = 'M'
59 DEL = 'D'
59 DEL = 'D'
60
60
61
61
62 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
62 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
63 """
63 """
64 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
64 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
65
65
66 :param ignore_whitespace: ignore whitespaces in diff
66 :param ignore_whitespace: ignore whitespaces in diff
67 """
67 """
68 # make sure we pass in default context
68 # make sure we pass in default context
69 context = context or 3
69 context = context or 3
70 # protect against IntOverflow when passing HUGE context
70 # protect against IntOverflow when passing HUGE context
71 if context > MAX_CONTEXT:
71 if context > MAX_CONTEXT:
72 context = MAX_CONTEXT
72 context = MAX_CONTEXT
73
73
74 submodules = filter(lambda o: isinstance(o, SubModuleNode),
74 submodules = filter(lambda o: isinstance(o, SubModuleNode),
75 [filenode_new, filenode_old])
75 [filenode_new, filenode_old])
76 if submodules:
76 if submodules:
77 return ''
77 return ''
78
78
79 for filenode in (filenode_old, filenode_new):
79 for filenode in (filenode_old, filenode_new):
80 if not isinstance(filenode, FileNode):
80 if not isinstance(filenode, FileNode):
81 raise VCSError(
81 raise VCSError(
82 "Given object should be FileNode object, not %s"
82 "Given object should be FileNode object, not %s"
83 % filenode.__class__)
83 % filenode.__class__)
84
84
85 repo = filenode_new.commit.repository
85 repo = filenode_new.commit.repository
86 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
86 old_commit = filenode_old.commit or repo.EMPTY_COMMIT
87 new_commit = filenode_new.commit
87 new_commit = filenode_new.commit
88
88
89 vcs_gitdiff = repo.get_diff(
89 vcs_gitdiff = repo.get_diff(
90 old_commit, new_commit, filenode_new.path,
90 old_commit, new_commit, filenode_new.path,
91 ignore_whitespace, context, path1=filenode_old.path)
91 ignore_whitespace, context, path1=filenode_old.path)
92 return vcs_gitdiff
92 return vcs_gitdiff
93
93
94 NEW_FILENODE = 1
94 NEW_FILENODE = 1
95 DEL_FILENODE = 2
95 DEL_FILENODE = 2
96 MOD_FILENODE = 3
96 MOD_FILENODE = 3
97 RENAMED_FILENODE = 4
97 RENAMED_FILENODE = 4
98 COPIED_FILENODE = 5
98 COPIED_FILENODE = 5
99 CHMOD_FILENODE = 6
99 CHMOD_FILENODE = 6
100 BIN_FILENODE = 7
100 BIN_FILENODE = 7
101
101
102
102
103 class LimitedDiffContainer(object):
103 class LimitedDiffContainer(object):
104
104
105 def __init__(self, diff_limit, cur_diff_size, diff):
105 def __init__(self, diff_limit, cur_diff_size, diff):
106 self.diff = diff
106 self.diff = diff
107 self.diff_limit = diff_limit
107 self.diff_limit = diff_limit
108 self.cur_diff_size = cur_diff_size
108 self.cur_diff_size = cur_diff_size
109
109
110 def __getitem__(self, key):
110 def __getitem__(self, key):
111 return self.diff.__getitem__(key)
111 return self.diff.__getitem__(key)
112
112
113 def __iter__(self):
113 def __iter__(self):
114 for l in self.diff:
114 for l in self.diff:
115 yield l
115 yield l
116
116
117
117
118 class Action(object):
118 class Action(object):
119 """
119 """
120 Contains constants for the action value of the lines in a parsed diff.
120 Contains constants for the action value of the lines in a parsed diff.
121 """
121 """
122
122
123 ADD = 'add'
123 ADD = 'add'
124 DELETE = 'del'
124 DELETE = 'del'
125 UNMODIFIED = 'unmod'
125 UNMODIFIED = 'unmod'
126
126
127 CONTEXT = 'context'
127 CONTEXT = 'context'
128 OLD_NO_NL = 'old-no-nl'
128 OLD_NO_NL = 'old-no-nl'
129 NEW_NO_NL = 'new-no-nl'
129 NEW_NO_NL = 'new-no-nl'
130
130
131
131
132 class DiffProcessor(object):
132 class DiffProcessor(object):
133 """
133 """
134 Give it a unified or git diff and it returns a list of the files that were
134 Give it a unified or git diff and it returns a list of the files that were
135 mentioned in the diff together with a dict of meta information that
135 mentioned in the diff together with a dict of meta information that
136 can be used to render it in a HTML template.
136 can be used to render it in a HTML template.
137
137
138 .. note:: Unicode handling
138 .. note:: Unicode handling
139
139
140 The original diffs are a byte sequence and can contain filenames
140 The original diffs are a byte sequence and can contain filenames
141 in mixed encodings. This class generally returns `unicode` objects
141 in mixed encodings. This class generally returns `unicode` objects
142 since the result is intended for presentation to the user.
142 since the result is intended for presentation to the user.
143
143
144 """
144 """
145 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
145 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
146 _newline_marker = re.compile(r'^\\ No newline at end of file')
146 _newline_marker = re.compile(r'^\\ No newline at end of file')
147
147
148 # used for inline highlighter word split
148 # used for inline highlighter word split
149 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
149 _token_re = re.compile(r'()(&gt;|&lt;|&amp;|\W+?)')
150
150
151 # collapse ranges of commits over given number
151 # collapse ranges of commits over given number
152 _collapse_commits_over = 5
152 _collapse_commits_over = 5
153
153
154 def __init__(self, diff, format='gitdiff', diff_limit=None,
154 def __init__(self, diff, format='gitdiff', diff_limit=None,
155 file_limit=None, show_full_diff=True):
155 file_limit=None, show_full_diff=True):
156 """
156 """
157 :param diff: A `Diff` object representing a diff from a vcs backend
157 :param diff: A `Diff` object representing a diff from a vcs backend
158 :param format: format of diff passed, `udiff` or `gitdiff`
158 :param format: format of diff passed, `udiff` or `gitdiff`
159 :param diff_limit: define the size of diff that is considered "big"
159 :param diff_limit: define the size of diff that is considered "big"
160 based on that parameter cut off will be triggered, set to None
160 based on that parameter cut off will be triggered, set to None
161 to show full diff
161 to show full diff
162 """
162 """
163 self._diff = diff
163 self._diff = diff
164 self._format = format
164 self._format = format
165 self.adds = 0
165 self.adds = 0
166 self.removes = 0
166 self.removes = 0
167 # calculate diff size
167 # calculate diff size
168 self.diff_limit = diff_limit
168 self.diff_limit = diff_limit
169 self.file_limit = file_limit
169 self.file_limit = file_limit
170 self.show_full_diff = show_full_diff
170 self.show_full_diff = show_full_diff
171 self.cur_diff_size = 0
171 self.cur_diff_size = 0
172 self.parsed = False
172 self.parsed = False
173 self.parsed_diff = []
173 self.parsed_diff = []
174
174
175 log.debug('Initialized DiffProcessor with %s mode', format)
175 log.debug('Initialized DiffProcessor with %s mode', format)
176 if format == 'gitdiff':
176 if format == 'gitdiff':
177 self.differ = self._highlight_line_difflib
177 self.differ = self._highlight_line_difflib
178 self._parser = self._parse_gitdiff
178 self._parser = self._parse_gitdiff
179 else:
179 else:
180 self.differ = self._highlight_line_udiff
180 self.differ = self._highlight_line_udiff
181 self._parser = self._new_parse_gitdiff
181 self._parser = self._new_parse_gitdiff
182
182
183 def _copy_iterator(self):
183 def _copy_iterator(self):
184 """
184 """
185 make a fresh copy of generator, we should not iterate thru
185 make a fresh copy of generator, we should not iterate thru
186 an original as it's needed for repeating operations on
186 an original as it's needed for repeating operations on
187 this instance of DiffProcessor
187 this instance of DiffProcessor
188 """
188 """
189 self.__udiff, iterator_copy = tee(self.__udiff)
189 self.__udiff, iterator_copy = tee(self.__udiff)
190 return iterator_copy
190 return iterator_copy
191
191
192 def _escaper(self, string):
192 def _escaper(self, string):
193 """
193 """
194 Escaper for diff escapes special chars and checks the diff limit
194 Escaper for diff escapes special chars and checks the diff limit
195
195
196 :param string:
196 :param string:
197 """
197 """
198 self.cur_diff_size += len(string)
198 self.cur_diff_size += len(string)
199
199
200 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
200 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
201 raise DiffLimitExceeded('Diff Limit Exceeded')
201 raise DiffLimitExceeded('Diff Limit Exceeded')
202
202
203 return string \
203 return string \
204 .replace('&', '&amp;')\
204 .replace('&', '&amp;')\
205 .replace('<', '&lt;')\
205 .replace('<', '&lt;')\
206 .replace('>', '&gt;')
206 .replace('>', '&gt;')
207
207
208 def _line_counter(self, l):
208 def _line_counter(self, l):
209 """
209 """
210 Checks each line and bumps total adds/removes for this diff
210 Checks each line and bumps total adds/removes for this diff
211
211
212 :param l:
212 :param l:
213 """
213 """
214 if l.startswith('+') and not l.startswith('+++'):
214 if l.startswith('+') and not l.startswith('+++'):
215 self.adds += 1
215 self.adds += 1
216 elif l.startswith('-') and not l.startswith('---'):
216 elif l.startswith('-') and not l.startswith('---'):
217 self.removes += 1
217 self.removes += 1
218 return safe_unicode(l)
218 return safe_unicode(l)
219
219
220 def _highlight_line_difflib(self, line, next_):
220 def _highlight_line_difflib(self, line, next_):
221 """
221 """
222 Highlight inline changes in both lines.
222 Highlight inline changes in both lines.
223 """
223 """
224
224
225 if line['action'] == Action.DELETE:
225 if line['action'] == Action.DELETE:
226 old, new = line, next_
226 old, new = line, next_
227 else:
227 else:
228 old, new = next_, line
228 old, new = next_, line
229
229
230 oldwords = self._token_re.split(old['line'])
230 oldwords = self._token_re.split(old['line'])
231 newwords = self._token_re.split(new['line'])
231 newwords = self._token_re.split(new['line'])
232 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
232 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
233
233
234 oldfragments, newfragments = [], []
234 oldfragments, newfragments = [], []
235 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
235 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
236 oldfrag = ''.join(oldwords[i1:i2])
236 oldfrag = ''.join(oldwords[i1:i2])
237 newfrag = ''.join(newwords[j1:j2])
237 newfrag = ''.join(newwords[j1:j2])
238 if tag != 'equal':
238 if tag != 'equal':
239 if oldfrag:
239 if oldfrag:
240 oldfrag = '<del>%s</del>' % oldfrag
240 oldfrag = '<del>%s</del>' % oldfrag
241 if newfrag:
241 if newfrag:
242 newfrag = '<ins>%s</ins>' % newfrag
242 newfrag = '<ins>%s</ins>' % newfrag
243 oldfragments.append(oldfrag)
243 oldfragments.append(oldfrag)
244 newfragments.append(newfrag)
244 newfragments.append(newfrag)
245
245
246 old['line'] = "".join(oldfragments)
246 old['line'] = "".join(oldfragments)
247 new['line'] = "".join(newfragments)
247 new['line'] = "".join(newfragments)
248
248
249 def _highlight_line_udiff(self, line, next_):
249 def _highlight_line_udiff(self, line, next_):
250 """
250 """
251 Highlight inline changes in both lines.
251 Highlight inline changes in both lines.
252 """
252 """
253 start = 0
253 start = 0
254 limit = min(len(line['line']), len(next_['line']))
254 limit = min(len(line['line']), len(next_['line']))
255 while start < limit and line['line'][start] == next_['line'][start]:
255 while start < limit and line['line'][start] == next_['line'][start]:
256 start += 1
256 start += 1
257 end = -1
257 end = -1
258 limit -= start
258 limit -= start
259 while -end <= limit and line['line'][end] == next_['line'][end]:
259 while -end <= limit and line['line'][end] == next_['line'][end]:
260 end -= 1
260 end -= 1
261 end += 1
261 end += 1
262 if start or end:
262 if start or end:
263 def do(l):
263 def do(l):
264 last = end + len(l['line'])
264 last = end + len(l['line'])
265 if l['action'] == Action.ADD:
265 if l['action'] == Action.ADD:
266 tag = 'ins'
266 tag = 'ins'
267 else:
267 else:
268 tag = 'del'
268 tag = 'del'
269 l['line'] = '%s<%s>%s</%s>%s' % (
269 l['line'] = '%s<%s>%s</%s>%s' % (
270 l['line'][:start],
270 l['line'][:start],
271 tag,
271 tag,
272 l['line'][start:last],
272 l['line'][start:last],
273 tag,
273 tag,
274 l['line'][last:]
274 l['line'][last:]
275 )
275 )
276 do(line)
276 do(line)
277 do(next_)
277 do(next_)
278
278
279 def _clean_line(self, line, command):
279 def _clean_line(self, line, command):
280 if command in ['+', '-', ' ']:
280 if command in ['+', '-', ' ']:
281 # only modify the line if it's actually a diff thing
281 # only modify the line if it's actually a diff thing
282 line = line[1:]
282 line = line[1:]
283 return line
283 return line
284
284
285 def _parse_gitdiff(self, inline_diff=True):
285 def _parse_gitdiff(self, inline_diff=True):
286 _files = []
286 _files = []
287 diff_container = lambda arg: arg
287 diff_container = lambda arg: arg
288
288
289 for chunk in self._diff.chunks():
289 for chunk in self._diff.chunks():
290 head = chunk.header
290 head = chunk.header
291
291
292 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
292 diff = imap(self._escaper, self.diff_splitter(chunk.diff))
293 raw_diff = chunk.raw
293 raw_diff = chunk.raw
294 limited_diff = False
294 limited_diff = False
295 exceeds_limit = False
295 exceeds_limit = False
296
296
297 op = None
297 op = None
298 stats = {
298 stats = {
299 'added': 0,
299 'added': 0,
300 'deleted': 0,
300 'deleted': 0,
301 'binary': False,
301 'binary': False,
302 'ops': {},
302 'ops': {},
303 }
303 }
304
304
305 if head['deleted_file_mode']:
305 if head['deleted_file_mode']:
306 op = OPS.DEL
306 op = OPS.DEL
307 stats['binary'] = True
307 stats['binary'] = True
308 stats['ops'][DEL_FILENODE] = 'deleted file'
308 stats['ops'][DEL_FILENODE] = 'deleted file'
309
309
310 elif head['new_file_mode']:
310 elif head['new_file_mode']:
311 op = OPS.ADD
311 op = OPS.ADD
312 stats['binary'] = True
312 stats['binary'] = True
313 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
313 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
314 else: # modify operation, can be copy, rename or chmod
314 else: # modify operation, can be copy, rename or chmod
315
315
316 # CHMOD
316 # CHMOD
317 if head['new_mode'] and head['old_mode']:
317 if head['new_mode'] and head['old_mode']:
318 op = OPS.MOD
318 op = OPS.MOD
319 stats['binary'] = True
319 stats['binary'] = True
320 stats['ops'][CHMOD_FILENODE] = (
320 stats['ops'][CHMOD_FILENODE] = (
321 'modified file chmod %s => %s' % (
321 'modified file chmod %s => %s' % (
322 head['old_mode'], head['new_mode']))
322 head['old_mode'], head['new_mode']))
323 # RENAME
323 # RENAME
324 if head['rename_from'] != head['rename_to']:
324 if head['rename_from'] != head['rename_to']:
325 op = OPS.MOD
325 op = OPS.MOD
326 stats['binary'] = True
326 stats['binary'] = True
327 stats['ops'][RENAMED_FILENODE] = (
327 stats['ops'][RENAMED_FILENODE] = (
328 'file renamed from %s to %s' % (
328 'file renamed from %s to %s' % (
329 head['rename_from'], head['rename_to']))
329 head['rename_from'], head['rename_to']))
330 # COPY
330 # COPY
331 if head.get('copy_from') and head.get('copy_to'):
331 if head.get('copy_from') and head.get('copy_to'):
332 op = OPS.MOD
332 op = OPS.MOD
333 stats['binary'] = True
333 stats['binary'] = True
334 stats['ops'][COPIED_FILENODE] = (
334 stats['ops'][COPIED_FILENODE] = (
335 'file copied from %s to %s' % (
335 'file copied from %s to %s' % (
336 head['copy_from'], head['copy_to']))
336 head['copy_from'], head['copy_to']))
337
337
338 # If our new parsed headers didn't match anything fallback to
338 # If our new parsed headers didn't match anything fallback to
339 # old style detection
339 # old style detection
340 if op is None:
340 if op is None:
341 if not head['a_file'] and head['b_file']:
341 if not head['a_file'] and head['b_file']:
342 op = OPS.ADD
342 op = OPS.ADD
343 stats['binary'] = True
343 stats['binary'] = True
344 stats['ops'][NEW_FILENODE] = 'new file'
344 stats['ops'][NEW_FILENODE] = 'new file'
345
345
346 elif head['a_file'] and not head['b_file']:
346 elif head['a_file'] and not head['b_file']:
347 op = OPS.DEL
347 op = OPS.DEL
348 stats['binary'] = True
348 stats['binary'] = True
349 stats['ops'][DEL_FILENODE] = 'deleted file'
349 stats['ops'][DEL_FILENODE] = 'deleted file'
350
350
351 # it's not ADD not DELETE
351 # it's not ADD not DELETE
352 if op is None:
352 if op is None:
353 op = OPS.MOD
353 op = OPS.MOD
354 stats['binary'] = True
354 stats['binary'] = True
355 stats['ops'][MOD_FILENODE] = 'modified file'
355 stats['ops'][MOD_FILENODE] = 'modified file'
356
356
357 # a real non-binary diff
357 # a real non-binary diff
358 if head['a_file'] or head['b_file']:
358 if head['a_file'] or head['b_file']:
359 try:
359 try:
360 raw_diff, chunks, _stats = self._parse_lines(diff)
360 raw_diff, chunks, _stats = self._parse_lines(diff)
361 stats['binary'] = False
361 stats['binary'] = False
362 stats['added'] = _stats[0]
362 stats['added'] = _stats[0]
363 stats['deleted'] = _stats[1]
363 stats['deleted'] = _stats[1]
364 # explicit mark that it's a modified file
364 # explicit mark that it's a modified file
365 if op == OPS.MOD:
365 if op == OPS.MOD:
366 stats['ops'][MOD_FILENODE] = 'modified file'
366 stats['ops'][MOD_FILENODE] = 'modified file'
367 exceeds_limit = len(raw_diff) > self.file_limit
367 exceeds_limit = len(raw_diff) > self.file_limit
368
368
369 # changed from _escaper function so we validate size of
369 # changed from _escaper function so we validate size of
370 # each file instead of the whole diff
370 # each file instead of the whole diff
371 # diff will hide big files but still show small ones
371 # diff will hide big files but still show small ones
372 # from my tests, big files are fairly safe to be parsed
372 # from my tests, big files are fairly safe to be parsed
373 # but the browser is the bottleneck
373 # but the browser is the bottleneck
374 if not self.show_full_diff and exceeds_limit:
374 if not self.show_full_diff and exceeds_limit:
375 raise DiffLimitExceeded('File Limit Exceeded')
375 raise DiffLimitExceeded('File Limit Exceeded')
376
376
377 except DiffLimitExceeded:
377 except DiffLimitExceeded:
378 diff_container = lambda _diff: \
378 diff_container = lambda _diff: \
379 LimitedDiffContainer(
379 LimitedDiffContainer(
380 self.diff_limit, self.cur_diff_size, _diff)
380 self.diff_limit, self.cur_diff_size, _diff)
381
381
382 exceeds_limit = len(raw_diff) > self.file_limit
382 exceeds_limit = len(raw_diff) > self.file_limit
383 limited_diff = True
383 limited_diff = True
384 chunks = []
384 chunks = []
385
385
386 else: # GIT format binary patch, or possibly empty diff
386 else: # GIT format binary patch, or possibly empty diff
387 if head['bin_patch']:
387 if head['bin_patch']:
388 # we have operation already extracted, but we mark simply
388 # we have operation already extracted, but we mark simply
389 # it's a diff we wont show for binary files
389 # it's a diff we wont show for binary files
390 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
390 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
391 chunks = []
391 chunks = []
392
392
393 if chunks and not self.show_full_diff and op == OPS.DEL:
393 if chunks and not self.show_full_diff and op == OPS.DEL:
394 # if not full diff mode show deleted file contents
394 # if not full diff mode show deleted file contents
395 # TODO: anderson: if the view is not too big, there is no way
395 # TODO: anderson: if the view is not too big, there is no way
396 # to see the content of the file
396 # to see the content of the file
397 chunks = []
397 chunks = []
398
398
399 chunks.insert(0, [{
399 chunks.insert(0, [{
400 'old_lineno': '',
400 'old_lineno': '',
401 'new_lineno': '',
401 'new_lineno': '',
402 'action': Action.CONTEXT,
402 'action': Action.CONTEXT,
403 'line': msg,
403 'line': msg,
404 } for _op, msg in stats['ops'].iteritems()
404 } for _op, msg in stats['ops'].iteritems()
405 if _op not in [MOD_FILENODE]])
405 if _op not in [MOD_FILENODE]])
406
406
407 _files.append({
407 _files.append({
408 'filename': safe_unicode(head['b_path']),
408 'filename': safe_unicode(head['b_path']),
409 'old_revision': head['a_blob_id'],
409 'old_revision': head['a_blob_id'],
410 'new_revision': head['b_blob_id'],
410 'new_revision': head['b_blob_id'],
411 'chunks': chunks,
411 'chunks': chunks,
412 'raw_diff': safe_unicode(raw_diff),
412 'raw_diff': safe_unicode(raw_diff),
413 'operation': op,
413 'operation': op,
414 'stats': stats,
414 'stats': stats,
415 'exceeds_limit': exceeds_limit,
415 'exceeds_limit': exceeds_limit,
416 'is_limited_diff': limited_diff,
416 'is_limited_diff': limited_diff,
417 })
417 })
418
418
419 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
419 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
420 OPS.DEL: 2}.get(info['operation'])
420 OPS.DEL: 2}.get(info['operation'])
421
421
422 if not inline_diff:
422 if not inline_diff:
423 return diff_container(sorted(_files, key=sorter))
423 return diff_container(sorted(_files, key=sorter))
424
424
425 # highlight inline changes
425 # highlight inline changes
426 for diff_data in _files:
426 for diff_data in _files:
427 for chunk in diff_data['chunks']:
427 for chunk in diff_data['chunks']:
428 lineiter = iter(chunk)
428 lineiter = iter(chunk)
429 try:
429 try:
430 while 1:
430 while 1:
431 line = lineiter.next()
431 line = lineiter.next()
432 if line['action'] not in (
432 if line['action'] not in (
433 Action.UNMODIFIED, Action.CONTEXT):
433 Action.UNMODIFIED, Action.CONTEXT):
434 nextline = lineiter.next()
434 nextline = lineiter.next()
435 if nextline['action'] in ['unmod', 'context'] or \
435 if nextline['action'] in ['unmod', 'context'] or \
436 nextline['action'] == line['action']:
436 nextline['action'] == line['action']:
437 continue
437 continue
438 self.differ(line, nextline)
438 self.differ(line, nextline)
439 except StopIteration:
439 except StopIteration:
440 pass
440 pass
441
441
442 return diff_container(sorted(_files, key=sorter))
442 return diff_container(sorted(_files, key=sorter))
443
443
444 def _check_large_diff(self):
444 def _check_large_diff(self):
445 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
445 log.debug('Diff exceeds current diff_limit of %s', self.diff_limit)
446 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
446 if not self.show_full_diff and (self.cur_diff_size > self.diff_limit):
447 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
447 raise DiffLimitExceeded('Diff Limit `%s` Exceeded', self.diff_limit)
448
448
449 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
449 # FIXME: NEWDIFFS: dan: this replaces _parse_gitdiff
450 def _new_parse_gitdiff(self, inline_diff=True):
450 def _new_parse_gitdiff(self, inline_diff=True):
451 _files = []
451 _files = []
452
452
453 # this can be overriden later to a LimitedDiffContainer type
453 # this can be overriden later to a LimitedDiffContainer type
454 diff_container = lambda arg: arg
454 diff_container = lambda arg: arg
455
455
456 for chunk in self._diff.chunks():
456 for chunk in self._diff.chunks():
457 head = chunk.header
457 head = chunk.header
458 log.debug('parsing diff %r', head)
458 log.debug('parsing diff %r', head)
459
459
460 raw_diff = chunk.raw
460 raw_diff = chunk.raw
461 limited_diff = False
461 limited_diff = False
462 exceeds_limit = False
462 exceeds_limit = False
463
463
464 op = None
464 op = None
465 stats = {
465 stats = {
466 'added': 0,
466 'added': 0,
467 'deleted': 0,
467 'deleted': 0,
468 'binary': False,
468 'binary': False,
469 'old_mode': None,
469 'old_mode': None,
470 'new_mode': None,
470 'new_mode': None,
471 'ops': {},
471 'ops': {},
472 }
472 }
473 if head['old_mode']:
473 if head['old_mode']:
474 stats['old_mode'] = head['old_mode']
474 stats['old_mode'] = head['old_mode']
475 if head['new_mode']:
475 if head['new_mode']:
476 stats['new_mode'] = head['new_mode']
476 stats['new_mode'] = head['new_mode']
477 if head['b_mode']:
477 if head['b_mode']:
478 stats['new_mode'] = head['b_mode']
478 stats['new_mode'] = head['b_mode']
479
479
480 # delete file
480 # delete file
481 if head['deleted_file_mode']:
481 if head['deleted_file_mode']:
482 op = OPS.DEL
482 op = OPS.DEL
483 stats['binary'] = True
483 stats['binary'] = True
484 stats['ops'][DEL_FILENODE] = 'deleted file'
484 stats['ops'][DEL_FILENODE] = 'deleted file'
485
485
486 # new file
486 # new file
487 elif head['new_file_mode']:
487 elif head['new_file_mode']:
488 op = OPS.ADD
488 op = OPS.ADD
489 stats['binary'] = True
489 stats['binary'] = True
490 stats['old_mode'] = None
490 stats['old_mode'] = None
491 stats['new_mode'] = head['new_file_mode']
491 stats['new_mode'] = head['new_file_mode']
492 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
492 stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
493
493
494 # modify operation, can be copy, rename or chmod
494 # modify operation, can be copy, rename or chmod
495 else:
495 else:
496 # CHMOD
496 # CHMOD
497 if head['new_mode'] and head['old_mode']:
497 if head['new_mode'] and head['old_mode']:
498 op = OPS.MOD
498 op = OPS.MOD
499 stats['binary'] = True
499 stats['binary'] = True
500 stats['ops'][CHMOD_FILENODE] = (
500 stats['ops'][CHMOD_FILENODE] = (
501 'modified file chmod %s => %s' % (
501 'modified file chmod %s => %s' % (
502 head['old_mode'], head['new_mode']))
502 head['old_mode'], head['new_mode']))
503
503
504 # RENAME
504 # RENAME
505 if head['rename_from'] != head['rename_to']:
505 if head['rename_from'] != head['rename_to']:
506 op = OPS.MOD
506 op = OPS.MOD
507 stats['binary'] = True
507 stats['binary'] = True
508 stats['renamed'] = (head['rename_from'], head['rename_to'])
508 stats['renamed'] = (head['rename_from'], head['rename_to'])
509 stats['ops'][RENAMED_FILENODE] = (
509 stats['ops'][RENAMED_FILENODE] = (
510 'file renamed from %s to %s' % (
510 'file renamed from %s to %s' % (
511 head['rename_from'], head['rename_to']))
511 head['rename_from'], head['rename_to']))
512 # COPY
512 # COPY
513 if head.get('copy_from') and head.get('copy_to'):
513 if head.get('copy_from') and head.get('copy_to'):
514 op = OPS.MOD
514 op = OPS.MOD
515 stats['binary'] = True
515 stats['binary'] = True
516 stats['copied'] = (head['copy_from'], head['copy_to'])
516 stats['copied'] = (head['copy_from'], head['copy_to'])
517 stats['ops'][COPIED_FILENODE] = (
517 stats['ops'][COPIED_FILENODE] = (
518 'file copied from %s to %s' % (
518 'file copied from %s to %s' % (
519 head['copy_from'], head['copy_to']))
519 head['copy_from'], head['copy_to']))
520
520
521 # If our new parsed headers didn't match anything fallback to
521 # If our new parsed headers didn't match anything fallback to
522 # old style detection
522 # old style detection
523 if op is None:
523 if op is None:
524 if not head['a_file'] and head['b_file']:
524 if not head['a_file'] and head['b_file']:
525 op = OPS.ADD
525 op = OPS.ADD
526 stats['binary'] = True
526 stats['binary'] = True
527 stats['new_file'] = True
527 stats['new_file'] = True
528 stats['ops'][NEW_FILENODE] = 'new file'
528 stats['ops'][NEW_FILENODE] = 'new file'
529
529
530 elif head['a_file'] and not head['b_file']:
530 elif head['a_file'] and not head['b_file']:
531 op = OPS.DEL
531 op = OPS.DEL
532 stats['binary'] = True
532 stats['binary'] = True
533 stats['ops'][DEL_FILENODE] = 'deleted file'
533 stats['ops'][DEL_FILENODE] = 'deleted file'
534
534
535 # it's not ADD not DELETE
535 # it's not ADD not DELETE
536 if op is None:
536 if op is None:
537 op = OPS.MOD
537 op = OPS.MOD
538 stats['binary'] = True
538 stats['binary'] = True
539 stats['ops'][MOD_FILENODE] = 'modified file'
539 stats['ops'][MOD_FILENODE] = 'modified file'
540
540
541 # a real non-binary diff
541 # a real non-binary diff
542 if head['a_file'] or head['b_file']:
542 if head['a_file'] or head['b_file']:
543 # simulate splitlines, so we keep the line end part
543 # simulate splitlines, so we keep the line end part
544 diff = self.diff_splitter(chunk.diff)
544 diff = self.diff_splitter(chunk.diff)
545
545
546 # append each file to the diff size
546 # append each file to the diff size
547 raw_chunk_size = len(raw_diff)
547 raw_chunk_size = len(raw_diff)
548
548
549 exceeds_limit = raw_chunk_size > self.file_limit
549 exceeds_limit = raw_chunk_size > self.file_limit
550 self.cur_diff_size += raw_chunk_size
550 self.cur_diff_size += raw_chunk_size
551
551
552 try:
552 try:
553 # Check each file instead of the whole diff.
553 # Check each file instead of the whole diff.
554 # Diff will hide big files but still show small ones.
554 # Diff will hide big files but still show small ones.
555 # From the tests big files are fairly safe to be parsed
555 # From the tests big files are fairly safe to be parsed
556 # but the browser is the bottleneck.
556 # but the browser is the bottleneck.
557 if not self.show_full_diff and exceeds_limit:
557 if not self.show_full_diff and exceeds_limit:
558 log.debug('File `%s` exceeds current file_limit of %s',
558 log.debug('File `%s` exceeds current file_limit of %s',
559 safe_unicode(head['b_path']), self.file_limit)
559 safe_unicode(head['b_path']), self.file_limit)
560 raise DiffLimitExceeded(
560 raise DiffLimitExceeded(
561 'File Limit %s Exceeded', self.file_limit)
561 'File Limit %s Exceeded', self.file_limit)
562
562
563 self._check_large_diff()
563 self._check_large_diff()
564
564
565 raw_diff, chunks, _stats = self._new_parse_lines(diff)
565 raw_diff, chunks, _stats = self._new_parse_lines(diff)
566 stats['binary'] = False
566 stats['binary'] = False
567 stats['added'] = _stats[0]
567 stats['added'] = _stats[0]
568 stats['deleted'] = _stats[1]
568 stats['deleted'] = _stats[1]
569 # explicit mark that it's a modified file
569 # explicit mark that it's a modified file
570 if op == OPS.MOD:
570 if op == OPS.MOD:
571 stats['ops'][MOD_FILENODE] = 'modified file'
571 stats['ops'][MOD_FILENODE] = 'modified file'
572
572
573 except DiffLimitExceeded:
573 except DiffLimitExceeded:
574 diff_container = lambda _diff: \
574 diff_container = lambda _diff: \
575 LimitedDiffContainer(
575 LimitedDiffContainer(
576 self.diff_limit, self.cur_diff_size, _diff)
576 self.diff_limit, self.cur_diff_size, _diff)
577
577
578 limited_diff = True
578 limited_diff = True
579 chunks = []
579 chunks = []
580
580
581 else: # GIT format binary patch, or possibly empty diff
581 else: # GIT format binary patch, or possibly empty diff
582 if head['bin_patch']:
582 if head['bin_patch']:
583 # we have operation already extracted, but we mark simply
583 # we have operation already extracted, but we mark simply
584 # it's a diff we wont show for binary files
584 # it's a diff we wont show for binary files
585 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
585 stats['ops'][BIN_FILENODE] = 'binary diff hidden'
586 chunks = []
586 chunks = []
587
587
588 # Hide content of deleted node by setting empty chunks
588 # Hide content of deleted node by setting empty chunks
589 if chunks and not self.show_full_diff and op == OPS.DEL:
589 if chunks and not self.show_full_diff and op == OPS.DEL:
590 # if not full diff mode show deleted file contents
590 # if not full diff mode show deleted file contents
591 # TODO: anderson: if the view is not too big, there is no way
591 # TODO: anderson: if the view is not too big, there is no way
592 # to see the content of the file
592 # to see the content of the file
593 chunks = []
593 chunks = []
594
594
595 chunks.insert(
595 chunks.insert(
596 0, [{'old_lineno': '',
596 0, [{'old_lineno': '',
597 'new_lineno': '',
597 'new_lineno': '',
598 'action': Action.CONTEXT,
598 'action': Action.CONTEXT,
599 'line': msg,
599 'line': msg,
600 } for _op, msg in stats['ops'].iteritems()
600 } for _op, msg in stats['ops'].iteritems()
601 if _op not in [MOD_FILENODE]])
601 if _op not in [MOD_FILENODE]])
602
602
603 original_filename = safe_unicode(head['a_path'])
603 original_filename = safe_unicode(head['a_path'])
604 _files.append({
604 _files.append({
605 'original_filename': original_filename,
605 'original_filename': original_filename,
606 'filename': safe_unicode(head['b_path']),
606 'filename': safe_unicode(head['b_path']),
607 'old_revision': head['a_blob_id'],
607 'old_revision': head['a_blob_id'],
608 'new_revision': head['b_blob_id'],
608 'new_revision': head['b_blob_id'],
609 'chunks': chunks,
609 'chunks': chunks,
610 'raw_diff': safe_unicode(raw_diff),
610 'raw_diff': safe_unicode(raw_diff),
611 'operation': op,
611 'operation': op,
612 'stats': stats,
612 'stats': stats,
613 'exceeds_limit': exceeds_limit,
613 'exceeds_limit': exceeds_limit,
614 'is_limited_diff': limited_diff,
614 'is_limited_diff': limited_diff,
615 })
615 })
616
616
617 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
617 sorter = lambda info: {OPS.ADD: 0, OPS.MOD: 1,
618 OPS.DEL: 2}.get(info['operation'])
618 OPS.DEL: 2}.get(info['operation'])
619
619
620 return diff_container(sorted(_files, key=sorter))
620 return diff_container(sorted(_files, key=sorter))
621
621
622 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
622 # FIXME: NEWDIFFS: dan: this gets replaced by _new_parse_lines
623 def _parse_lines(self, diff_iter):
623 def _parse_lines(self, diff_iter):
624 """
624 """
625 Parse the diff an return data for the template.
625 Parse the diff an return data for the template.
626 """
626 """
627
627
628 stats = [0, 0]
628 stats = [0, 0]
629 chunks = []
629 chunks = []
630 raw_diff = []
630 raw_diff = []
631
631
632 try:
632 try:
633 line = diff_iter.next()
633 line = diff_iter.next()
634
634
635 while line:
635 while line:
636 raw_diff.append(line)
636 raw_diff.append(line)
637 lines = []
637 lines = []
638 chunks.append(lines)
638 chunks.append(lines)
639
639
640 match = self._chunk_re.match(line)
640 match = self._chunk_re.match(line)
641
641
642 if not match:
642 if not match:
643 break
643 break
644
644
645 gr = match.groups()
645 gr = match.groups()
646 (old_line, old_end,
646 (old_line, old_end,
647 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
647 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
648 old_line -= 1
648 old_line -= 1
649 new_line -= 1
649 new_line -= 1
650
650
651 context = len(gr) == 5
651 context = len(gr) == 5
652 old_end += old_line
652 old_end += old_line
653 new_end += new_line
653 new_end += new_line
654
654
655 if context:
655 if context:
656 # skip context only if it's first line
656 # skip context only if it's first line
657 if int(gr[0]) > 1:
657 if int(gr[0]) > 1:
658 lines.append({
658 lines.append({
659 'old_lineno': '...',
659 'old_lineno': '...',
660 'new_lineno': '...',
660 'new_lineno': '...',
661 'action': Action.CONTEXT,
661 'action': Action.CONTEXT,
662 'line': line,
662 'line': line,
663 })
663 })
664
664
665 line = diff_iter.next()
665 line = diff_iter.next()
666
666
667 while old_line < old_end or new_line < new_end:
667 while old_line < old_end or new_line < new_end:
668 command = ' '
668 command = ' '
669 if line:
669 if line:
670 command = line[0]
670 command = line[0]
671
671
672 affects_old = affects_new = False
672 affects_old = affects_new = False
673
673
674 # ignore those if we don't expect them
674 # ignore those if we don't expect them
675 if command in '#@':
675 if command in '#@':
676 continue
676 continue
677 elif command == '+':
677 elif command == '+':
678 affects_new = True
678 affects_new = True
679 action = Action.ADD
679 action = Action.ADD
680 stats[0] += 1
680 stats[0] += 1
681 elif command == '-':
681 elif command == '-':
682 affects_old = True
682 affects_old = True
683 action = Action.DELETE
683 action = Action.DELETE
684 stats[1] += 1
684 stats[1] += 1
685 else:
685 else:
686 affects_old = affects_new = True
686 affects_old = affects_new = True
687 action = Action.UNMODIFIED
687 action = Action.UNMODIFIED
688
688
689 if not self._newline_marker.match(line):
689 if not self._newline_marker.match(line):
690 old_line += affects_old
690 old_line += affects_old
691 new_line += affects_new
691 new_line += affects_new
692 lines.append({
692 lines.append({
693 'old_lineno': affects_old and old_line or '',
693 'old_lineno': affects_old and old_line or '',
694 'new_lineno': affects_new and new_line or '',
694 'new_lineno': affects_new and new_line or '',
695 'action': action,
695 'action': action,
696 'line': self._clean_line(line, command)
696 'line': self._clean_line(line, command)
697 })
697 })
698 raw_diff.append(line)
698 raw_diff.append(line)
699
699
700 line = diff_iter.next()
700 line = diff_iter.next()
701
701
702 if self._newline_marker.match(line):
702 if self._newline_marker.match(line):
703 # we need to append to lines, since this is not
703 # we need to append to lines, since this is not
704 # counted in the line specs of diff
704 # counted in the line specs of diff
705 lines.append({
705 lines.append({
706 'old_lineno': '...',
706 'old_lineno': '...',
707 'new_lineno': '...',
707 'new_lineno': '...',
708 'action': Action.CONTEXT,
708 'action': Action.CONTEXT,
709 'line': self._clean_line(line, command)
709 'line': self._clean_line(line, command)
710 })
710 })
711
711
712 except StopIteration:
712 except StopIteration:
713 pass
713 pass
714 return ''.join(raw_diff), chunks, stats
714 return ''.join(raw_diff), chunks, stats
715
715
716 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
716 # FIXME: NEWDIFFS: dan: this replaces _parse_lines
717 def _new_parse_lines(self, diff_iter):
717 def _new_parse_lines(self, diff_iter):
718 """
718 """
719 Parse the diff an return data for the template.
719 Parse the diff an return data for the template.
720 """
720 """
721
721
722 stats = [0, 0]
722 stats = [0, 0]
723 chunks = []
723 chunks = []
724 raw_diff = []
724 raw_diff = []
725
725
726 try:
726 try:
727 line = diff_iter.next()
727 line = diff_iter.next()
728
728
729 while line:
729 while line:
730 raw_diff.append(line)
730 raw_diff.append(line)
731 # match header e.g @@ -0,0 +1 @@\n'
731 # match header e.g @@ -0,0 +1 @@\n'
732 match = self._chunk_re.match(line)
732 match = self._chunk_re.match(line)
733
733
734 if not match:
734 if not match:
735 break
735 break
736
736
737 gr = match.groups()
737 gr = match.groups()
738 (old_line, old_end,
738 (old_line, old_end,
739 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
739 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
740
740
741 lines = []
741 lines = []
742 hunk = {
742 hunk = {
743 'section_header': gr[-1],
743 'section_header': gr[-1],
744 'source_start': old_line,
744 'source_start': old_line,
745 'source_length': old_end,
745 'source_length': old_end,
746 'target_start': new_line,
746 'target_start': new_line,
747 'target_length': new_end,
747 'target_length': new_end,
748 'lines': lines,
748 'lines': lines,
749 }
749 }
750 chunks.append(hunk)
750 chunks.append(hunk)
751
751
752 old_line -= 1
752 old_line -= 1
753 new_line -= 1
753 new_line -= 1
754
754
755 context = len(gr) == 5
755 context = len(gr) == 5
756 old_end += old_line
756 old_end += old_line
757 new_end += new_line
757 new_end += new_line
758
758
759 line = diff_iter.next()
759 line = diff_iter.next()
760
760
761 while old_line < old_end or new_line < new_end:
761 while old_line < old_end or new_line < new_end:
762 command = ' '
762 command = ' '
763 if line:
763 if line:
764 command = line[0]
764 command = line[0]
765
765
766 affects_old = affects_new = False
766 affects_old = affects_new = False
767
767
768 # ignore those if we don't expect them
768 # ignore those if we don't expect them
769 if command in '#@':
769 if command in '#@':
770 continue
770 continue
771 elif command == '+':
771 elif command == '+':
772 affects_new = True
772 affects_new = True
773 action = Action.ADD
773 action = Action.ADD
774 stats[0] += 1
774 stats[0] += 1
775 elif command == '-':
775 elif command == '-':
776 affects_old = True
776 affects_old = True
777 action = Action.DELETE
777 action = Action.DELETE
778 stats[1] += 1
778 stats[1] += 1
779 else:
779 else:
780 affects_old = affects_new = True
780 affects_old = affects_new = True
781 action = Action.UNMODIFIED
781 action = Action.UNMODIFIED
782
782
783 if not self._newline_marker.match(line):
783 if not self._newline_marker.match(line):
784 old_line += affects_old
784 old_line += affects_old
785 new_line += affects_new
785 new_line += affects_new
786 lines.append({
786 lines.append({
787 'old_lineno': affects_old and old_line or '',
787 'old_lineno': affects_old and old_line or '',
788 'new_lineno': affects_new and new_line or '',
788 'new_lineno': affects_new and new_line or '',
789 'action': action,
789 'action': action,
790 'line': self._clean_line(line, command)
790 'line': self._clean_line(line, command)
791 })
791 })
792 raw_diff.append(line)
792 raw_diff.append(line)
793
793
794 line = diff_iter.next()
794 line = diff_iter.next()
795
795
796 if self._newline_marker.match(line):
796 if self._newline_marker.match(line):
797 # we need to append to lines, since this is not
797 # we need to append to lines, since this is not
798 # counted in the line specs of diff
798 # counted in the line specs of diff
799 if affects_old:
799 if affects_old:
800 action = Action.OLD_NO_NL
800 action = Action.OLD_NO_NL
801 elif affects_new:
801 elif affects_new:
802 action = Action.NEW_NO_NL
802 action = Action.NEW_NO_NL
803 else:
803 else:
804 raise Exception('invalid context for no newline')
804 raise Exception('invalid context for no newline')
805
805
806 lines.append({
806 lines.append({
807 'old_lineno': None,
807 'old_lineno': None,
808 'new_lineno': None,
808 'new_lineno': None,
809 'action': action,
809 'action': action,
810 'line': self._clean_line(line, command)
810 'line': self._clean_line(line, command)
811 })
811 })
812
812
813 except StopIteration:
813 except StopIteration:
814 pass
814 pass
815
815
816 return ''.join(raw_diff), chunks, stats
816 return ''.join(raw_diff), chunks, stats
817
817
818 def _safe_id(self, idstring):
818 def _safe_id(self, idstring):
819 """Make a string safe for including in an id attribute.
819 """Make a string safe for including in an id attribute.
820
820
821 The HTML spec says that id attributes 'must begin with
821 The HTML spec says that id attributes 'must begin with
822 a letter ([A-Za-z]) and may be followed by any number
822 a letter ([A-Za-z]) and may be followed by any number
823 of letters, digits ([0-9]), hyphens ("-"), underscores
823 of letters, digits ([0-9]), hyphens ("-"), underscores
824 ("_"), colons (":"), and periods (".")'. These regexps
824 ("_"), colons (":"), and periods (".")'. These regexps
825 are slightly over-zealous, in that they remove colons
825 are slightly over-zealous, in that they remove colons
826 and periods unnecessarily.
826 and periods unnecessarily.
827
827
828 Whitespace is transformed into underscores, and then
828 Whitespace is transformed into underscores, and then
829 anything which is not a hyphen or a character that
829 anything which is not a hyphen or a character that
830 matches \w (alphanumerics and underscore) is removed.
830 matches \w (alphanumerics and underscore) is removed.
831
831
832 """
832 """
833 # Transform all whitespace to underscore
833 # Transform all whitespace to underscore
834 idstring = re.sub(r'\s', "_", '%s' % idstring)
834 idstring = re.sub(r'\s', "_", '%s' % idstring)
835 # Remove everything that is not a hyphen or a member of \w
835 # Remove everything that is not a hyphen or a member of \w
836 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
836 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
837 return idstring
837 return idstring
838
838
839 @classmethod
839 @classmethod
840 def diff_splitter(cls, string):
840 def diff_splitter(cls, string):
841 """
841 """
842 Diff split that emulates .splitlines() but works only on \n
842 Diff split that emulates .splitlines() but works only on \n
843 """
843 """
844 if not string:
844 if not string:
845 return
845 return
846 elif string == '\n':
846 elif string == '\n':
847 yield u'\n'
847 yield u'\n'
848 else:
848 else:
849
849
850 has_newline = string.endswith('\n')
850 has_newline = string.endswith('\n')
851 elements = string.split('\n')
851 elements = string.split('\n')
852 if has_newline:
852 if has_newline:
853 # skip last element as it's empty string from newlines
853 # skip last element as it's empty string from newlines
854 elements = elements[:-1]
854 elements = elements[:-1]
855
855
856 len_elements = len(elements)
856 len_elements = len(elements)
857
857
858 for cnt, line in enumerate(elements, start=1):
858 for cnt, line in enumerate(elements, start=1):
859 last_line = cnt == len_elements
859 last_line = cnt == len_elements
860 if last_line and not has_newline:
860 if last_line and not has_newline:
861 yield safe_unicode(line)
861 yield safe_unicode(line)
862 else:
862 else:
863 yield safe_unicode(line) + '\n'
863 yield safe_unicode(line) + '\n'
864
864
865 def prepare(self, inline_diff=True):
865 def prepare(self, inline_diff=True):
866 """
866 """
867 Prepare the passed udiff for HTML rendering.
867 Prepare the passed udiff for HTML rendering.
868
868
869 :return: A list of dicts with diff information.
869 :return: A list of dicts with diff information.
870 """
870 """
871 parsed = self._parser(inline_diff=inline_diff)
871 parsed = self._parser(inline_diff=inline_diff)
872 self.parsed = True
872 self.parsed = True
873 self.parsed_diff = parsed
873 self.parsed_diff = parsed
874 return parsed
874 return parsed
875
875
876 def as_raw(self, diff_lines=None):
876 def as_raw(self, diff_lines=None):
877 """
877 """
878 Returns raw diff as a byte string
878 Returns raw diff as a byte string
879 """
879 """
880 return self._diff.raw
880 return self._diff.raw
881
881
882 def as_html(self, table_class='code-difftable', line_class='line',
882 def as_html(self, table_class='code-difftable', line_class='line',
883 old_lineno_class='lineno old', new_lineno_class='lineno new',
883 old_lineno_class='lineno old', new_lineno_class='lineno new',
884 code_class='code', enable_comments=False, parsed_lines=None):
884 code_class='code', enable_comments=False, parsed_lines=None):
885 """
885 """
886 Return given diff as html table with customized css classes
886 Return given diff as html table with customized css classes
887 """
887 """
888 # TODO(marcink): not sure how to pass in translator
888 # TODO(marcink): not sure how to pass in translator
889 # here in an efficient way, leave the _ for proper gettext extraction
889 # here in an efficient way, leave the _ for proper gettext extraction
890 _ = lambda s: s
890 _ = lambda s: s
891
891
892 def _link_to_if(condition, label, url):
892 def _link_to_if(condition, label, url):
893 """
893 """
894 Generates a link if condition is meet or just the label if not.
894 Generates a link if condition is meet or just the label if not.
895 """
895 """
896
896
897 if condition:
897 if condition:
898 return '''<a href="%(url)s" class="tooltip"
898 return '''<a href="%(url)s" class="tooltip"
899 title="%(title)s">%(label)s</a>''' % {
899 title="%(title)s">%(label)s</a>''' % {
900 'title': _('Click to select line'),
900 'title': _('Click to select line'),
901 'url': url,
901 'url': url,
902 'label': label
902 'label': label
903 }
903 }
904 else:
904 else:
905 return label
905 return label
906 if not self.parsed:
906 if not self.parsed:
907 self.prepare()
907 self.prepare()
908
908
909 diff_lines = self.parsed_diff
909 diff_lines = self.parsed_diff
910 if parsed_lines:
910 if parsed_lines:
911 diff_lines = parsed_lines
911 diff_lines = parsed_lines
912
912
913 _html_empty = True
913 _html_empty = True
914 _html = []
914 _html = []
915 _html.append('''<table class="%(table_class)s">\n''' % {
915 _html.append('''<table class="%(table_class)s">\n''' % {
916 'table_class': table_class
916 'table_class': table_class
917 })
917 })
918
918
919 for diff in diff_lines:
919 for diff in diff_lines:
920 for line in diff['chunks']:
920 for line in diff['chunks']:
921 _html_empty = False
921 _html_empty = False
922 for change in line:
922 for change in line:
923 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
923 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
924 'lc': line_class,
924 'lc': line_class,
925 'action': change['action']
925 'action': change['action']
926 })
926 })
927 anchor_old_id = ''
927 anchor_old_id = ''
928 anchor_new_id = ''
928 anchor_new_id = ''
929 anchor_old = "%(filename)s_o%(oldline_no)s" % {
929 anchor_old = "%(filename)s_o%(oldline_no)s" % {
930 'filename': self._safe_id(diff['filename']),
930 'filename': self._safe_id(diff['filename']),
931 'oldline_no': change['old_lineno']
931 'oldline_no': change['old_lineno']
932 }
932 }
933 anchor_new = "%(filename)s_n%(oldline_no)s" % {
933 anchor_new = "%(filename)s_n%(oldline_no)s" % {
934 'filename': self._safe_id(diff['filename']),
934 'filename': self._safe_id(diff['filename']),
935 'oldline_no': change['new_lineno']
935 'oldline_no': change['new_lineno']
936 }
936 }
937 cond_old = (change['old_lineno'] != '...' and
937 cond_old = (change['old_lineno'] != '...' and
938 change['old_lineno'])
938 change['old_lineno'])
939 cond_new = (change['new_lineno'] != '...' and
939 cond_new = (change['new_lineno'] != '...' and
940 change['new_lineno'])
940 change['new_lineno'])
941 if cond_old:
941 if cond_old:
942 anchor_old_id = 'id="%s"' % anchor_old
942 anchor_old_id = 'id="%s"' % anchor_old
943 if cond_new:
943 if cond_new:
944 anchor_new_id = 'id="%s"' % anchor_new
944 anchor_new_id = 'id="%s"' % anchor_new
945
945
946 if change['action'] != Action.CONTEXT:
946 if change['action'] != Action.CONTEXT:
947 anchor_link = True
947 anchor_link = True
948 else:
948 else:
949 anchor_link = False
949 anchor_link = False
950
950
951 ###########################################################
951 ###########################################################
952 # COMMENT ICONS
952 # COMMENT ICONS
953 ###########################################################
953 ###########################################################
954 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
954 _html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
955
955
956 if enable_comments and change['action'] != Action.CONTEXT:
956 if enable_comments and change['action'] != Action.CONTEXT:
957 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
957 _html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
958
958
959 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
959 _html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comment Thread"><i class="icon-comment"></i></td>\n''')
960
960
961 ###########################################################
961 ###########################################################
962 # OLD LINE NUMBER
962 # OLD LINE NUMBER
963 ###########################################################
963 ###########################################################
964 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
964 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
965 'a_id': anchor_old_id,
965 'a_id': anchor_old_id,
966 'olc': old_lineno_class
966 'olc': old_lineno_class
967 })
967 })
968
968
969 _html.append('''%(link)s''' % {
969 _html.append('''%(link)s''' % {
970 'link': _link_to_if(anchor_link, change['old_lineno'],
970 'link': _link_to_if(anchor_link, change['old_lineno'],
971 '#%s' % anchor_old)
971 '#%s' % anchor_old)
972 })
972 })
973 _html.append('''</td>\n''')
973 _html.append('''</td>\n''')
974 ###########################################################
974 ###########################################################
975 # NEW LINE NUMBER
975 # NEW LINE NUMBER
976 ###########################################################
976 ###########################################################
977
977
978 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
978 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
979 'a_id': anchor_new_id,
979 'a_id': anchor_new_id,
980 'nlc': new_lineno_class
980 'nlc': new_lineno_class
981 })
981 })
982
982
983 _html.append('''%(link)s''' % {
983 _html.append('''%(link)s''' % {
984 'link': _link_to_if(anchor_link, change['new_lineno'],
984 'link': _link_to_if(anchor_link, change['new_lineno'],
985 '#%s' % anchor_new)
985 '#%s' % anchor_new)
986 })
986 })
987 _html.append('''</td>\n''')
987 _html.append('''</td>\n''')
988 ###########################################################
988 ###########################################################
989 # CODE
989 # CODE
990 ###########################################################
990 ###########################################################
991 code_classes = [code_class]
991 code_classes = [code_class]
992 if (not enable_comments or
992 if (not enable_comments or
993 change['action'] == Action.CONTEXT):
993 change['action'] == Action.CONTEXT):
994 code_classes.append('no-comment')
994 code_classes.append('no-comment')
995 _html.append('\t<td class="%s">' % ' '.join(code_classes))
995 _html.append('\t<td class="%s">' % ' '.join(code_classes))
996 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
996 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
997 'code': change['line']
997 'code': change['line']
998 })
998 })
999
999
1000 _html.append('''\t</td>''')
1000 _html.append('''\t</td>''')
1001 _html.append('''\n</tr>\n''')
1001 _html.append('''\n</tr>\n''')
1002 _html.append('''</table>''')
1002 _html.append('''</table>''')
1003 if _html_empty:
1003 if _html_empty:
1004 return None
1004 return None
1005 return ''.join(_html)
1005 return ''.join(_html)
1006
1006
1007 def stat(self):
1007 def stat(self):
1008 """
1008 """
1009 Returns tuple of added, and removed lines for this instance
1009 Returns tuple of added, and removed lines for this instance
1010 """
1010 """
1011 return self.adds, self.removes
1011 return self.adds, self.removes
1012
1012
1013 def get_context_of_line(
1013 def get_context_of_line(
1014 self, path, diff_line=None, context_before=3, context_after=3):
1014 self, path, diff_line=None, context_before=3, context_after=3):
1015 """
1015 """
1016 Returns the context lines for the specified diff line.
1016 Returns the context lines for the specified diff line.
1017
1017
1018 :type diff_line: :class:`DiffLineNumber`
1018 :type diff_line: :class:`DiffLineNumber`
1019 """
1019 """
1020 assert self.parsed, "DiffProcessor is not initialized."
1020 assert self.parsed, "DiffProcessor is not initialized."
1021
1021
1022 if None not in diff_line:
1022 if None not in diff_line:
1023 raise ValueError(
1023 raise ValueError(
1024 "Cannot specify both line numbers: {}".format(diff_line))
1024 "Cannot specify both line numbers: {}".format(diff_line))
1025
1025
1026 file_diff = self._get_file_diff(path)
1026 file_diff = self._get_file_diff(path)
1027 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1027 chunk, idx = self._find_chunk_line_index(file_diff, diff_line)
1028
1028
1029 first_line_to_include = max(idx - context_before, 0)
1029 first_line_to_include = max(idx - context_before, 0)
1030 first_line_after_context = idx + context_after + 1
1030 first_line_after_context = idx + context_after + 1
1031 context_lines = chunk[first_line_to_include:first_line_after_context]
1031 context_lines = chunk[first_line_to_include:first_line_after_context]
1032
1032
1033 line_contents = [
1033 line_contents = [
1034 _context_line(line) for line in context_lines
1034 _context_line(line) for line in context_lines
1035 if _is_diff_content(line)]
1035 if _is_diff_content(line)]
1036 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1036 # TODO: johbo: Interim fixup, the diff chunks drop the final newline.
1037 # Once they are fixed, we can drop this line here.
1037 # Once they are fixed, we can drop this line here.
1038 if line_contents:
1038 if line_contents:
1039 line_contents[-1] = (
1039 line_contents[-1] = (
1040 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1040 line_contents[-1][0], line_contents[-1][1].rstrip('\n') + '\n')
1041 return line_contents
1041 return line_contents
1042
1042
1043 def find_context(self, path, context, offset=0):
1043 def find_context(self, path, context, offset=0):
1044 """
1044 """
1045 Finds the given `context` inside of the diff.
1045 Finds the given `context` inside of the diff.
1046
1046
1047 Use the parameter `offset` to specify which offset the target line has
1047 Use the parameter `offset` to specify which offset the target line has
1048 inside of the given `context`. This way the correct diff line will be
1048 inside of the given `context`. This way the correct diff line will be
1049 returned.
1049 returned.
1050
1050
1051 :param offset: Shall be used to specify the offset of the main line
1051 :param offset: Shall be used to specify the offset of the main line
1052 within the given `context`.
1052 within the given `context`.
1053 """
1053 """
1054 if offset < 0 or offset >= len(context):
1054 if offset < 0 or offset >= len(context):
1055 raise ValueError(
1055 raise ValueError(
1056 "Only positive values up to the length of the context "
1056 "Only positive values up to the length of the context "
1057 "minus one are allowed.")
1057 "minus one are allowed.")
1058
1058
1059 matches = []
1059 matches = []
1060 file_diff = self._get_file_diff(path)
1060 file_diff = self._get_file_diff(path)
1061
1061
1062 for chunk in file_diff['chunks']:
1062 for chunk in file_diff['chunks']:
1063 context_iter = iter(context)
1063 context_iter = iter(context)
1064 for line_idx, line in enumerate(chunk):
1064 for line_idx, line in enumerate(chunk):
1065 try:
1065 try:
1066 if _context_line(line) == context_iter.next():
1066 if _context_line(line) == context_iter.next():
1067 continue
1067 continue
1068 except StopIteration:
1068 except StopIteration:
1069 matches.append((line_idx, chunk))
1069 matches.append((line_idx, chunk))
1070 context_iter = iter(context)
1070 context_iter = iter(context)
1071
1071
1072 # Increment position and triger StopIteration
1072 # Increment position and triger StopIteration
1073 # if we had a match at the end
1073 # if we had a match at the end
1074 line_idx += 1
1074 line_idx += 1
1075 try:
1075 try:
1076 context_iter.next()
1076 context_iter.next()
1077 except StopIteration:
1077 except StopIteration:
1078 matches.append((line_idx, chunk))
1078 matches.append((line_idx, chunk))
1079
1079
1080 effective_offset = len(context) - offset
1080 effective_offset = len(context) - offset
1081 found_at_diff_lines = [
1081 found_at_diff_lines = [
1082 _line_to_diff_line_number(chunk[idx - effective_offset])
1082 _line_to_diff_line_number(chunk[idx - effective_offset])
1083 for idx, chunk in matches]
1083 for idx, chunk in matches]
1084
1084
1085 return found_at_diff_lines
1085 return found_at_diff_lines
1086
1086
1087 def _get_file_diff(self, path):
1087 def _get_file_diff(self, path):
1088 for file_diff in self.parsed_diff:
1088 for file_diff in self.parsed_diff:
1089 if file_diff['filename'] == path:
1089 if file_diff['filename'] == path:
1090 break
1090 break
1091 else:
1091 else:
1092 raise FileNotInDiffException("File {} not in diff".format(path))
1092 raise FileNotInDiffException("File {} not in diff".format(path))
1093 return file_diff
1093 return file_diff
1094
1094
1095 def _find_chunk_line_index(self, file_diff, diff_line):
1095 def _find_chunk_line_index(self, file_diff, diff_line):
1096 for chunk in file_diff['chunks']:
1096 for chunk in file_diff['chunks']:
1097 for idx, line in enumerate(chunk):
1097 for idx, line in enumerate(chunk):
1098 if line['old_lineno'] == diff_line.old:
1098 if line['old_lineno'] == diff_line.old:
1099 return chunk, idx
1099 return chunk, idx
1100 if line['new_lineno'] == diff_line.new:
1100 if line['new_lineno'] == diff_line.new:
1101 return chunk, idx
1101 return chunk, idx
1102 raise LineNotInDiffException(
1102 raise LineNotInDiffException(
1103 "The line {} is not part of the diff.".format(diff_line))
1103 "The line {} is not part of the diff.".format(diff_line))
1104
1104
1105
1105
1106 def _is_diff_content(line):
1106 def _is_diff_content(line):
1107 return line['action'] in (
1107 return line['action'] in (
1108 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1108 Action.UNMODIFIED, Action.ADD, Action.DELETE)
1109
1109
1110
1110
1111 def _context_line(line):
1111 def _context_line(line):
1112 return (line['action'], line['line'])
1112 return (line['action'], line['line'])
1113
1113
1114
1114
1115 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1115 DiffLineNumber = collections.namedtuple('DiffLineNumber', ['old', 'new'])
1116
1116
1117
1117
1118 def _line_to_diff_line_number(line):
1118 def _line_to_diff_line_number(line):
1119 new_line_no = line['new_lineno'] or None
1119 new_line_no = line['new_lineno'] or None
1120 old_line_no = line['old_lineno'] or None
1120 old_line_no = line['old_lineno'] or None
1121 return DiffLineNumber(old=old_line_no, new=new_line_no)
1121 return DiffLineNumber(old=old_line_no, new=new_line_no)
1122
1122
1123
1123
1124 class FileNotInDiffException(Exception):
1124 class FileNotInDiffException(Exception):
1125 """
1125 """
1126 Raised when the context for a missing file is requested.
1126 Raised when the context for a missing file is requested.
1127
1127
1128 If you request the context for a line in a file which is not part of the
1128 If you request the context for a line in a file which is not part of the
1129 given diff, then this exception is raised.
1129 given diff, then this exception is raised.
1130 """
1130 """
1131
1131
1132
1132
1133 class LineNotInDiffException(Exception):
1133 class LineNotInDiffException(Exception):
1134 """
1134 """
1135 Raised when the context for a missing line is requested.
1135 Raised when the context for a missing line is requested.
1136
1136
1137 If you request the context for a line in a file and this line is not
1137 If you request the context for a line in a file and this line is not
1138 part of the given diff, then this exception is raised.
1138 part of the given diff, then this exception is raised.
1139 """
1139 """
1140
1140
1141
1141
1142 class DiffLimitExceeded(Exception):
1142 class DiffLimitExceeded(Exception):
1143 pass
1143 pass
1144
1144
1145
1145
1146 # NOTE(marcink): if diffs.mako change, probably this
1146 # NOTE(marcink): if diffs.mako change, probably this
1147 # needs a bump to next version
1147 # needs a bump to next version
1148 CURRENT_DIFF_VERSION = 'v3'
1148 CURRENT_DIFF_VERSION = 'v4'
1149
1149
1150
1150
1151 def _cleanup_cache_file(cached_diff_file):
1151 def _cleanup_cache_file(cached_diff_file):
1152 # cleanup file to not store it "damaged"
1152 # cleanup file to not store it "damaged"
1153 try:
1153 try:
1154 os.remove(cached_diff_file)
1154 os.remove(cached_diff_file)
1155 except Exception:
1155 except Exception:
1156 log.exception('Failed to cleanup path %s', cached_diff_file)
1156 log.exception('Failed to cleanup path %s', cached_diff_file)
1157
1157
1158
1158
1159 def cache_diff(cached_diff_file, diff, commits):
1159 def cache_diff(cached_diff_file, diff, commits):
1160
1160
1161 struct = {
1161 struct = {
1162 'version': CURRENT_DIFF_VERSION,
1162 'version': CURRENT_DIFF_VERSION,
1163 'diff': diff,
1163 'diff': diff,
1164 'commits': commits
1164 'commits': commits
1165 }
1165 }
1166
1166
1167 try:
1167 try:
1168 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1168 with bz2.BZ2File(cached_diff_file, 'wb') as f:
1169 pickle.dump(struct, f)
1169 pickle.dump(struct, f)
1170 log.debug('Saved diff cache under %s', cached_diff_file)
1170 log.debug('Saved diff cache under %s', cached_diff_file)
1171 except Exception:
1171 except Exception:
1172 log.warn('Failed to save cache', exc_info=True)
1172 log.warn('Failed to save cache', exc_info=True)
1173 _cleanup_cache_file(cached_diff_file)
1173 _cleanup_cache_file(cached_diff_file)
1174
1174
1175
1175
1176 def load_cached_diff(cached_diff_file):
1176 def load_cached_diff(cached_diff_file):
1177
1177
1178 default_struct = {
1178 default_struct = {
1179 'version': CURRENT_DIFF_VERSION,
1179 'version': CURRENT_DIFF_VERSION,
1180 'diff': None,
1180 'diff': None,
1181 'commits': None
1181 'commits': None
1182 }
1182 }
1183
1183
1184 has_cache = os.path.isfile(cached_diff_file)
1184 has_cache = os.path.isfile(cached_diff_file)
1185 if not has_cache:
1185 if not has_cache:
1186 return default_struct
1186 return default_struct
1187
1187
1188 data = None
1188 data = None
1189 try:
1189 try:
1190 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1190 with bz2.BZ2File(cached_diff_file, 'rb') as f:
1191 data = pickle.load(f)
1191 data = pickle.load(f)
1192 log.debug('Loaded diff cache from %s', cached_diff_file)
1192 log.debug('Loaded diff cache from %s', cached_diff_file)
1193 except Exception:
1193 except Exception:
1194 log.warn('Failed to read diff cache file', exc_info=True)
1194 log.warn('Failed to read diff cache file', exc_info=True)
1195
1195
1196 if not data:
1196 if not data:
1197 data = default_struct
1197 data = default_struct
1198
1198
1199 if not isinstance(data, dict):
1199 if not isinstance(data, dict):
1200 # old version of data ?
1200 # old version of data ?
1201 data = default_struct
1201 data = default_struct
1202
1202
1203 # check version
1203 # check version
1204 if data.get('version') != CURRENT_DIFF_VERSION:
1204 if data.get('version') != CURRENT_DIFF_VERSION:
1205 # purge cache
1205 # purge cache
1206 _cleanup_cache_file(cached_diff_file)
1206 _cleanup_cache_file(cached_diff_file)
1207 return default_struct
1207 return default_struct
1208
1208
1209 return data
1209 return data
1210
1210
1211
1211
1212 def generate_diff_cache_key(*args):
1212 def generate_diff_cache_key(*args):
1213 """
1213 """
1214 Helper to generate a cache key using arguments
1214 Helper to generate a cache key using arguments
1215 """
1215 """
1216 def arg_mapper(input_param):
1216 def arg_mapper(input_param):
1217 input_param = safe_str(input_param)
1217 input_param = safe_str(input_param)
1218 # we cannot allow '/' in arguments since it would allow
1218 # we cannot allow '/' in arguments since it would allow
1219 # subdirectory usage
1219 # subdirectory usage
1220 input_param.replace('/', '_')
1220 input_param.replace('/', '_')
1221 return input_param or None # prevent empty string arguments
1221 return input_param or None # prevent empty string arguments
1222
1222
1223 return '_'.join([
1223 return '_'.join([
1224 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1224 '{}' for i in range(len(args))]).format(*map(arg_mapper, args))
1225
1225
1226
1226
1227 def diff_cache_exist(cache_storage, *args):
1227 def diff_cache_exist(cache_storage, *args):
1228 """
1228 """
1229 Based on all generated arguments check and return a cache path
1229 Based on all generated arguments check and return a cache path
1230 """
1230 """
1231 cache_key = generate_diff_cache_key(*args)
1231 cache_key = generate_diff_cache_key(*args)
1232 cache_file_path = os.path.join(cache_storage, cache_key)
1232 cache_file_path = os.path.join(cache_storage, cache_key)
1233 # prevent path traversal attacks using some param that have e.g '../../'
1233 # prevent path traversal attacks using some param that have e.g '../../'
1234 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1234 if not os.path.abspath(cache_file_path).startswith(cache_storage):
1235 raise ValueError('Final path must be within {}'.format(cache_storage))
1235 raise ValueError('Final path must be within {}'.format(cache_storage))
1236
1236
1237 return cache_file_path
1237 return cache_file_path
@@ -1,1231 +1,1242 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-family: @text-semibold;
82 font-family: @text-semibold;
83 }
83 }
84
84
85 .compare-buttons {
85 .compare-buttons {
86 float: left;
86 float: left;
87 margin: 0;
87 margin: 0;
88 padding: 0 0 @padding;
88 padding: 0 0 @padding;
89
89
90 .btn {
90 .btn {
91 margin: 0 @padding 0 0;
91 margin: 0 @padding 0 0;
92 }
92 }
93 }
93 }
94 }
94 }
95
95
96 }
96 }
97
97
98 .parents {
98 .parents {
99 float: left;
99 float: left;
100 width: 100px;
100 width: 100px;
101 font-weight: 400;
101 font-weight: 400;
102 vertical-align: middle;
102 vertical-align: middle;
103 padding: 0px 2px 0px 2px;
103 padding: 0px 2px 0px 2px;
104 background-color: @grey6;
104 background-color: @grey6;
105
105
106 #parent_link {
106 #parent_link {
107 margin: 00px 2px;
107 margin: 00px 2px;
108
108
109 &.double {
109 &.double {
110 margin: 0px 2px;
110 margin: 0px 2px;
111 }
111 }
112
112
113 &.disabled{
113 &.disabled{
114 margin-right: @padding;
114 margin-right: @padding;
115 }
115 }
116 }
116 }
117 }
117 }
118
118
119 .children {
119 .children {
120 float: right;
120 float: right;
121 width: 100px;
121 width: 100px;
122 font-weight: 400;
122 font-weight: 400;
123 vertical-align: middle;
123 vertical-align: middle;
124 text-align: right;
124 text-align: right;
125 padding: 0px 2px 0px 2px;
125 padding: 0px 2px 0px 2px;
126 background-color: @grey6;
126 background-color: @grey6;
127
127
128 #child_link {
128 #child_link {
129 margin: 0px 2px;
129 margin: 0px 2px;
130
130
131 &.double {
131 &.double {
132 margin: 0px 2px;
132 margin: 0px 2px;
133 }
133 }
134
134
135 &.disabled{
135 &.disabled{
136 margin-right: @padding;
136 margin-right: @padding;
137 }
137 }
138 }
138 }
139 }
139 }
140
140
141 .changeset_header {
141 .changeset_header {
142 height: 16px;
142 height: 16px;
143
143
144 & > div{
144 & > div{
145 margin-right: @padding;
145 margin-right: @padding;
146 }
146 }
147 }
147 }
148
148
149 .changeset_file {
149 .changeset_file {
150 text-align: left;
150 text-align: left;
151 float: left;
151 float: left;
152 padding: 0;
152 padding: 0;
153
153
154 a{
154 a{
155 display: inline-block;
155 display: inline-block;
156 margin-right: 0.5em;
156 margin-right: 0.5em;
157 }
157 }
158
158
159 #selected_mode{
159 #selected_mode{
160 margin-left: 0;
160 margin-left: 0;
161 }
161 }
162 }
162 }
163
163
164 .diff-menu-wrapper {
164 .diff-menu-wrapper {
165 float: left;
165 float: left;
166 }
166 }
167
167
168 .diff-menu {
168 .diff-menu {
169 position: absolute;
169 position: absolute;
170 background: none repeat scroll 0 0 #FFFFFF;
170 background: none repeat scroll 0 0 #FFFFFF;
171 border-color: #003367 @grey3 @grey3;
171 border-color: #003367 @grey3 @grey3;
172 border-right: 1px solid @grey3;
172 border-right: 1px solid @grey3;
173 border-style: solid solid solid;
173 border-style: solid solid solid;
174 border-width: @border-thickness;
174 border-width: @border-thickness;
175 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
175 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
176 margin-top: 5px;
176 margin-top: 5px;
177 margin-left: 1px;
177 margin-left: 1px;
178 }
178 }
179
179
180 .diff-actions, .editor-actions {
180 .diff-actions, .editor-actions {
181 float: left;
181 float: left;
182
182
183 input{
183 input{
184 margin: 0 0.5em 0 0;
184 margin: 0 0.5em 0 0;
185 }
185 }
186 }
186 }
187
187
188 // END CODE-HEADER STYLES
188 // END CODE-HEADER STYLES
189
189
190 // BEGIN CODE-BODY STYLES
190 // BEGIN CODE-BODY STYLES
191
191
192 .code-body {
192 .code-body {
193 padding: 0;
193 padding: 0;
194 background-color: #ffffff;
194 background-color: #ffffff;
195 position: relative;
195 position: relative;
196 max-width: none;
196 max-width: none;
197 box-sizing: border-box;
197 box-sizing: border-box;
198 // TODO: johbo: Parent has overflow: auto, this forces the child here
198 // TODO: johbo: Parent has overflow: auto, this forces the child here
199 // to have the intended size and to scroll. Should be simplified.
199 // to have the intended size and to scroll. Should be simplified.
200 width: 100%;
200 width: 100%;
201 overflow-x: auto;
201 overflow-x: auto;
202 }
202 }
203
203
204 pre.raw {
204 pre.raw {
205 background: white;
205 background: white;
206 color: @grey1;
206 color: @grey1;
207 }
207 }
208 // END CODE-BODY STYLES
208 // END CODE-BODY STYLES
209
209
210 }
210 }
211
211
212
212
213 table.code-difftable {
213 table.code-difftable {
214 border-collapse: collapse;
214 border-collapse: collapse;
215 width: 99%;
215 width: 99%;
216 border-radius: 0px !important;
216 border-radius: 0px !important;
217
217
218 td {
218 td {
219 padding: 0 !important;
219 padding: 0 !important;
220 background: none !important;
220 background: none !important;
221 border: 0 !important;
221 border: 0 !important;
222 }
222 }
223
223
224 .context {
224 .context {
225 background: none repeat scroll 0 0 #DDE7EF;
225 background: none repeat scroll 0 0 #DDE7EF;
226 }
226 }
227
227
228 .add {
228 .add {
229 background: none repeat scroll 0 0 #DDFFDD;
229 background: none repeat scroll 0 0 #DDFFDD;
230
230
231 ins {
231 ins {
232 background: none repeat scroll 0 0 #AAFFAA;
232 background: none repeat scroll 0 0 #AAFFAA;
233 text-decoration: none;
233 text-decoration: none;
234 }
234 }
235 }
235 }
236
236
237 .del {
237 .del {
238 background: none repeat scroll 0 0 #FFDDDD;
238 background: none repeat scroll 0 0 #FFDDDD;
239
239
240 del {
240 del {
241 background: none repeat scroll 0 0 #FFAAAA;
241 background: none repeat scroll 0 0 #FFAAAA;
242 text-decoration: none;
242 text-decoration: none;
243 }
243 }
244 }
244 }
245
245
246 /** LINE NUMBERS **/
246 /** LINE NUMBERS **/
247 .lineno {
247 .lineno {
248 padding-left: 2px !important;
248 padding-left: 2px !important;
249 padding-right: 2px;
249 padding-right: 2px;
250 text-align: right;
250 text-align: right;
251 width: 32px;
251 width: 32px;
252 -moz-user-select: none;
252 -moz-user-select: none;
253 -webkit-user-select: none;
253 -webkit-user-select: none;
254 border-right: @border-thickness solid @grey5 !important;
254 border-right: @border-thickness solid @grey5 !important;
255 border-left: 0px solid #CCC !important;
255 border-left: 0px solid #CCC !important;
256 border-top: 0px solid #CCC !important;
256 border-top: 0px solid #CCC !important;
257 border-bottom: none !important;
257 border-bottom: none !important;
258
258
259 a {
259 a {
260 &:extend(pre);
260 &:extend(pre);
261 text-align: right;
261 text-align: right;
262 padding-right: 2px;
262 padding-right: 2px;
263 cursor: pointer;
263 cursor: pointer;
264 display: block;
264 display: block;
265 width: 32px;
265 width: 32px;
266 }
266 }
267 }
267 }
268
268
269 .context {
269 .context {
270 cursor: auto;
270 cursor: auto;
271 &:extend(pre);
271 &:extend(pre);
272 }
272 }
273
273
274 .lineno-inline {
274 .lineno-inline {
275 background: none repeat scroll 0 0 #FFF !important;
275 background: none repeat scroll 0 0 #FFF !important;
276 padding-left: 2px;
276 padding-left: 2px;
277 padding-right: 2px;
277 padding-right: 2px;
278 text-align: right;
278 text-align: right;
279 width: 30px;
279 width: 30px;
280 -moz-user-select: none;
280 -moz-user-select: none;
281 -webkit-user-select: none;
281 -webkit-user-select: none;
282 }
282 }
283
283
284 /** CODE **/
284 /** CODE **/
285 .code {
285 .code {
286 display: block;
286 display: block;
287 width: 100%;
287 width: 100%;
288
288
289 td {
289 td {
290 margin: 0;
290 margin: 0;
291 padding: 0;
291 padding: 0;
292 }
292 }
293
293
294 pre {
294 pre {
295 margin: 0;
295 margin: 0;
296 padding: 0;
296 padding: 0;
297 margin-left: .5em;
297 margin-left: .5em;
298 }
298 }
299 }
299 }
300 }
300 }
301
301
302
302
303 // Comments
303 // Comments
304
304
305 div.comment:target {
305 div.comment:target {
306 border-left: 6px solid @comment-highlight-color !important;
306 border-left: 6px solid @comment-highlight-color !important;
307 padding-left: 3px;
307 padding-left: 3px;
308 margin-left: -9px;
308 margin-left: -9px;
309 }
309 }
310
310
311 //TODO: anderson: can't get an absolute number out of anything, so had to put the
311 //TODO: anderson: can't get an absolute number out of anything, so had to put the
312 //current values that might change. But to make it clear I put as a calculation
312 //current values that might change. But to make it clear I put as a calculation
313 @comment-max-width: 1065px;
313 @comment-max-width: 1065px;
314 @pr-extra-margin: 34px;
314 @pr-extra-margin: 34px;
315 @pr-border-spacing: 4px;
315 @pr-border-spacing: 4px;
316 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
316 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
317
317
318 // Pull Request
318 // Pull Request
319 .cs_files .code-difftable {
319 .cs_files .code-difftable {
320 border: @border-thickness solid @grey5; //borders only on PRs
320 border: @border-thickness solid @grey5; //borders only on PRs
321
321
322 .comment-inline-form,
322 .comment-inline-form,
323 div.comment {
323 div.comment {
324 width: @pr-comment-width;
324 width: @pr-comment-width;
325 }
325 }
326 }
326 }
327
327
328 // Changeset
328 // Changeset
329 .code-difftable {
329 .code-difftable {
330 .comment-inline-form,
330 .comment-inline-form,
331 div.comment {
331 div.comment {
332 width: @comment-max-width;
332 width: @comment-max-width;
333 }
333 }
334 }
334 }
335
335
336 //Style page
336 //Style page
337 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
337 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
338 #style-page .code-difftable{
338 #style-page .code-difftable{
339 .comment-inline-form,
339 .comment-inline-form,
340 div.comment {
340 div.comment {
341 width: @comment-max-width - @style-extra-margin;
341 width: @comment-max-width - @style-extra-margin;
342 }
342 }
343 }
343 }
344
344
345 #context-bar > h2 {
345 #context-bar > h2 {
346 font-size: 20px;
346 font-size: 20px;
347 }
347 }
348
348
349 #context-bar > h2> a {
349 #context-bar > h2> a {
350 font-size: 20px;
350 font-size: 20px;
351 }
351 }
352 // end of defaults
352 // end of defaults
353
353
354 .file_diff_buttons {
354 .file_diff_buttons {
355 padding: 0 0 @padding;
355 padding: 0 0 @padding;
356
356
357 .drop-menu {
357 .drop-menu {
358 float: left;
358 float: left;
359 margin: 0 @padding 0 0;
359 margin: 0 @padding 0 0;
360 }
360 }
361 .btn {
361 .btn {
362 margin: 0 @padding 0 0;
362 margin: 0 @padding 0 0;
363 }
363 }
364 }
364 }
365
365
366 .code-body.textarea.editor {
366 .code-body.textarea.editor {
367 max-width: none;
367 max-width: none;
368 padding: 15px;
368 padding: 15px;
369 }
369 }
370
370
371 td.injected_diff{
371 td.injected_diff{
372 max-width: 1178px;
372 max-width: 1178px;
373 overflow-x: auto;
373 overflow-x: auto;
374 overflow-y: hidden;
374 overflow-y: hidden;
375
375
376 div.diff-container,
376 div.diff-container,
377 div.diffblock{
377 div.diffblock{
378 max-width: 100%;
378 max-width: 100%;
379 }
379 }
380
380
381 div.code-body {
381 div.code-body {
382 max-width: 1124px;
382 max-width: 1124px;
383 overflow-x: auto;
383 overflow-x: auto;
384 overflow-y: hidden;
384 overflow-y: hidden;
385 padding: 0;
385 padding: 0;
386 }
386 }
387 div.diffblock {
387 div.diffblock {
388 border: none;
388 border: none;
389 }
389 }
390
390
391 &.inline-form {
391 &.inline-form {
392 width: 99%
392 width: 99%
393 }
393 }
394 }
394 }
395
395
396
396
397 table.code-difftable {
397 table.code-difftable {
398 width: 100%;
398 width: 100%;
399 }
399 }
400
400
401 /** PYGMENTS COLORING **/
401 /** PYGMENTS COLORING **/
402 div.codeblock {
402 div.codeblock {
403
403
404 // TODO: johbo: Added interim to get rid of the margin around
404 // TODO: johbo: Added interim to get rid of the margin around
405 // Select2 widgets. This needs further cleanup.
405 // Select2 widgets. This needs further cleanup.
406 margin-top: @padding;
406 margin-top: @padding;
407
407
408 overflow: auto;
408 overflow: auto;
409 padding: 0px;
409 padding: 0px;
410 border: @border-thickness solid @grey5;
410 border: @border-thickness solid @grey5;
411 background: @grey6;
411 background: @grey6;
412 .border-radius(@border-radius);
412 .border-radius(@border-radius);
413
413
414 #remove_gist {
414 #remove_gist {
415 float: right;
415 float: right;
416 }
416 }
417
417
418 .gist_url {
418 .gist_url {
419 padding: 0px 0px 10px 0px;
419 padding: 0px 0px 10px 0px;
420 }
420 }
421
421
422 .author {
422 .author {
423 clear: both;
423 clear: both;
424 vertical-align: middle;
424 vertical-align: middle;
425 font-family: @text-bold;
425 font-family: @text-bold;
426 }
426 }
427
427
428 .btn-mini {
428 .btn-mini {
429 float: left;
429 float: left;
430 margin: 0 5px 0 0;
430 margin: 0 5px 0 0;
431 }
431 }
432
432
433 .code-header {
433 .code-header {
434 padding: @padding;
434 padding: @padding;
435 border-bottom: @border-thickness solid @grey5;
435 border-bottom: @border-thickness solid @grey5;
436
436
437 .rc-user {
437 .rc-user {
438 min-width: 0;
438 min-width: 0;
439 margin-right: .5em;
439 margin-right: .5em;
440 }
440 }
441
441
442 .stats {
442 .stats {
443 clear: both;
443 clear: both;
444 margin: 0 0 @padding 0;
444 margin: 0 0 @padding 0;
445 padding: 0;
445 padding: 0;
446 .left {
446 .left {
447 float: left;
447 float: left;
448 clear: left;
448 clear: left;
449 max-width: 75%;
449 max-width: 75%;
450 margin: 0 0 @padding 0;
450 margin: 0 0 @padding 0;
451
451
452 &.item {
452 &.item {
453 margin-right: @padding;
453 margin-right: @padding;
454 &.last { border-right: none; }
454 &.last { border-right: none; }
455 }
455 }
456 }
456 }
457 .buttons { float: right; }
457 .buttons { float: right; }
458 .author {
458 .author {
459 height: 25px; margin-left: 15px; font-weight: bold;
459 height: 25px; margin-left: 15px; font-weight: bold;
460 }
460 }
461 }
461 }
462
462
463 .commit {
463 .commit {
464 margin: 5px 0 0 26px;
464 margin: 5px 0 0 26px;
465 font-weight: normal;
465 font-weight: normal;
466 white-space: pre-wrap;
466 white-space: pre-wrap;
467 }
467 }
468 }
468 }
469
469
470 .message {
470 .message {
471 position: relative;
471 position: relative;
472 margin: @padding;
472 margin: @padding;
473
473
474 .codeblock-label {
474 .codeblock-label {
475 margin: 0 0 1em 0;
475 margin: 0 0 1em 0;
476 }
476 }
477 }
477 }
478
478
479 .code-body {
479 .code-body {
480 padding: @padding;
480 padding: @padding;
481 background-color: #ffffff;
481 background-color: #ffffff;
482 min-width: 100%;
482 min-width: 100%;
483 box-sizing: border-box;
483 box-sizing: border-box;
484 // TODO: johbo: Parent has overflow: auto, this forces the child here
484 // TODO: johbo: Parent has overflow: auto, this forces the child here
485 // to have the intended size and to scroll. Should be simplified.
485 // to have the intended size and to scroll. Should be simplified.
486 width: 100%;
486 width: 100%;
487 overflow-x: auto;
487 overflow-x: auto;
488
488
489 img.rendered-binary {
489 img.rendered-binary {
490 height: auto;
490 height: auto;
491 width: 100%;
491 width: 100%;
492 }
492 }
493 }
493 }
494 }
494 }
495
495
496 .code-highlighttable,
496 .code-highlighttable,
497 div.codeblock {
497 div.codeblock {
498
498
499 &.readme {
499 &.readme {
500 background-color: white;
500 background-color: white;
501 }
501 }
502
502
503 .markdown-block table {
503 .markdown-block table {
504 border-collapse: collapse;
504 border-collapse: collapse;
505
505
506 th,
506 th,
507 td {
507 td {
508 padding: .5em;
508 padding: .5em;
509 border: @border-thickness solid @border-default-color;
509 border: @border-thickness solid @border-default-color;
510 }
510 }
511 }
511 }
512
512
513 table {
513 table {
514 border: 0px;
514 border: 0px;
515 margin: 0;
515 margin: 0;
516 letter-spacing: normal;
516 letter-spacing: normal;
517
517
518
518
519 td {
519 td {
520 border: 0px;
520 border: 0px;
521 vertical-align: top;
521 vertical-align: top;
522 }
522 }
523 }
523 }
524 }
524 }
525
525
526 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
526 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
527 div.search-code-body {
527 div.search-code-body {
528 background-color: #ffffff; padding: 5px 0 5px 10px;
528 background-color: #ffffff; padding: 5px 0 5px 10px;
529 pre {
529 pre {
530 .match { background-color: #faffa6;}
530 .match { background-color: #faffa6;}
531 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
531 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
532 }
532 }
533 .code-highlighttable {
533 .code-highlighttable {
534 border-collapse: collapse;
534 border-collapse: collapse;
535
535
536 tr:hover {
536 tr:hover {
537 background: #fafafa;
537 background: #fafafa;
538 }
538 }
539 td.code {
539 td.code {
540 padding-left: 10px;
540 padding-left: 10px;
541 }
541 }
542 td.line {
542 td.line {
543 border-right: 1px solid #ccc !important;
543 border-right: 1px solid #ccc !important;
544 padding-right: 10px;
544 padding-right: 10px;
545 text-align: right;
545 text-align: right;
546 font-family: "Lucida Console",Monaco,monospace;
546 font-family: "Lucida Console",Monaco,monospace;
547 span {
547 span {
548 white-space: pre-wrap;
548 white-space: pre-wrap;
549 color: #666666;
549 color: #666666;
550 }
550 }
551 }
551 }
552 }
552 }
553 }
553 }
554
554
555 div.annotatediv { margin-left: 2px; margin-right: 4px; }
555 div.annotatediv { margin-left: 2px; margin-right: 4px; }
556 .code-highlight {
556 .code-highlight {
557 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
557 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
558 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
558 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
559 pre div:target {background-color: @comment-highlight-color !important;}
559 pre div:target {background-color: @comment-highlight-color !important;}
560 }
560 }
561
561
562 .linenos a { text-decoration: none; }
562 .linenos a { text-decoration: none; }
563
563
564 .CodeMirror-selected { background: @rchighlightblue; }
564 .CodeMirror-selected { background: @rchighlightblue; }
565 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
565 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
566 .CodeMirror ::selection { background: @rchighlightblue; }
566 .CodeMirror ::selection { background: @rchighlightblue; }
567 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
567 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
568
568
569 .code { display: block; border:0px !important; }
569 .code { display: block; border:0px !important; }
570 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
570 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
571 .codehilite {
571 .codehilite {
572 .hll { background-color: #ffffcc }
572 .hll { background-color: #ffffcc }
573 .c { color: #408080; font-style: italic } /* Comment */
573 .c { color: #408080; font-style: italic } /* Comment */
574 .err, .codehilite .err { border: none } /* Error */
574 .err, .codehilite .err { border: none } /* Error */
575 .k { color: #008000; font-weight: bold } /* Keyword */
575 .k { color: #008000; font-weight: bold } /* Keyword */
576 .o { color: #666666 } /* Operator */
576 .o { color: #666666 } /* Operator */
577 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
577 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
578 .cp { color: #BC7A00 } /* Comment.Preproc */
578 .cp { color: #BC7A00 } /* Comment.Preproc */
579 .c1 { color: #408080; font-style: italic } /* Comment.Single */
579 .c1 { color: #408080; font-style: italic } /* Comment.Single */
580 .cs { color: #408080; font-style: italic } /* Comment.Special */
580 .cs { color: #408080; font-style: italic } /* Comment.Special */
581 .gd { color: #A00000 } /* Generic.Deleted */
581 .gd { color: #A00000 } /* Generic.Deleted */
582 .ge { font-style: italic } /* Generic.Emph */
582 .ge { font-style: italic } /* Generic.Emph */
583 .gr { color: #FF0000 } /* Generic.Error */
583 .gr { color: #FF0000 } /* Generic.Error */
584 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
584 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
585 .gi { color: #00A000 } /* Generic.Inserted */
585 .gi { color: #00A000 } /* Generic.Inserted */
586 .go { color: #808080 } /* Generic.Output */
586 .go { color: #808080 } /* Generic.Output */
587 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
587 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
588 .gs { font-weight: bold } /* Generic.Strong */
588 .gs { font-weight: bold } /* Generic.Strong */
589 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
589 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
590 .gt { color: #0040D0 } /* Generic.Traceback */
590 .gt { color: #0040D0 } /* Generic.Traceback */
591 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
591 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
592 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
592 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
593 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
593 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
594 .kp { color: #008000 } /* Keyword.Pseudo */
594 .kp { color: #008000 } /* Keyword.Pseudo */
595 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
595 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
596 .kt { color: #B00040 } /* Keyword.Type */
596 .kt { color: #B00040 } /* Keyword.Type */
597 .m { color: #666666 } /* Literal.Number */
597 .m { color: #666666 } /* Literal.Number */
598 .s { color: #BA2121 } /* Literal.String */
598 .s { color: #BA2121 } /* Literal.String */
599 .na { color: #7D9029 } /* Name.Attribute */
599 .na { color: #7D9029 } /* Name.Attribute */
600 .nb { color: #008000 } /* Name.Builtin */
600 .nb { color: #008000 } /* Name.Builtin */
601 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
601 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
602 .no { color: #880000 } /* Name.Constant */
602 .no { color: #880000 } /* Name.Constant */
603 .nd { color: #AA22FF } /* Name.Decorator */
603 .nd { color: #AA22FF } /* Name.Decorator */
604 .ni { color: #999999; font-weight: bold } /* Name.Entity */
604 .ni { color: #999999; font-weight: bold } /* Name.Entity */
605 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
605 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
606 .nf { color: #0000FF } /* Name.Function */
606 .nf { color: #0000FF } /* Name.Function */
607 .nl { color: #A0A000 } /* Name.Label */
607 .nl { color: #A0A000 } /* Name.Label */
608 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
608 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
609 .nt { color: #008000; font-weight: bold } /* Name.Tag */
609 .nt { color: #008000; font-weight: bold } /* Name.Tag */
610 .nv { color: #19177C } /* Name.Variable */
610 .nv { color: #19177C } /* Name.Variable */
611 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
611 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
612 .w { color: #bbbbbb } /* Text.Whitespace */
612 .w { color: #bbbbbb } /* Text.Whitespace */
613 .mf { color: #666666 } /* Literal.Number.Float */
613 .mf { color: #666666 } /* Literal.Number.Float */
614 .mh { color: #666666 } /* Literal.Number.Hex */
614 .mh { color: #666666 } /* Literal.Number.Hex */
615 .mi { color: #666666 } /* Literal.Number.Integer */
615 .mi { color: #666666 } /* Literal.Number.Integer */
616 .mo { color: #666666 } /* Literal.Number.Oct */
616 .mo { color: #666666 } /* Literal.Number.Oct */
617 .sb { color: #BA2121 } /* Literal.String.Backtick */
617 .sb { color: #BA2121 } /* Literal.String.Backtick */
618 .sc { color: #BA2121 } /* Literal.String.Char */
618 .sc { color: #BA2121 } /* Literal.String.Char */
619 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
619 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
620 .s2 { color: #BA2121 } /* Literal.String.Double */
620 .s2 { color: #BA2121 } /* Literal.String.Double */
621 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
621 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
622 .sh { color: #BA2121 } /* Literal.String.Heredoc */
622 .sh { color: #BA2121 } /* Literal.String.Heredoc */
623 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
623 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
624 .sx { color: #008000 } /* Literal.String.Other */
624 .sx { color: #008000 } /* Literal.String.Other */
625 .sr { color: #BB6688 } /* Literal.String.Regex */
625 .sr { color: #BB6688 } /* Literal.String.Regex */
626 .s1 { color: #BA2121 } /* Literal.String.Single */
626 .s1 { color: #BA2121 } /* Literal.String.Single */
627 .ss { color: #19177C } /* Literal.String.Symbol */
627 .ss { color: #19177C } /* Literal.String.Symbol */
628 .bp { color: #008000 } /* Name.Builtin.Pseudo */
628 .bp { color: #008000 } /* Name.Builtin.Pseudo */
629 .vc { color: #19177C } /* Name.Variable.Class */
629 .vc { color: #19177C } /* Name.Variable.Class */
630 .vg { color: #19177C } /* Name.Variable.Global */
630 .vg { color: #19177C } /* Name.Variable.Global */
631 .vi { color: #19177C } /* Name.Variable.Instance */
631 .vi { color: #19177C } /* Name.Variable.Instance */
632 .il { color: #666666 } /* Literal.Number.Integer.Long */
632 .il { color: #666666 } /* Literal.Number.Integer.Long */
633 }
633 }
634
634
635 /* customized pre blocks for markdown/rst */
635 /* customized pre blocks for markdown/rst */
636 pre.literal-block, .codehilite pre{
636 pre.literal-block, .codehilite pre{
637 padding: @padding;
637 padding: @padding;
638 border: 1px solid @grey6;
638 border: 1px solid @grey6;
639 .border-radius(@border-radius);
639 .border-radius(@border-radius);
640 background-color: @grey7;
640 background-color: @grey7;
641 }
641 }
642
642
643
643
644 /* START NEW CODE BLOCK CSS */
644 /* START NEW CODE BLOCK CSS */
645
645
646 @cb-line-height: 18px;
646 @cb-line-height: 18px;
647 @cb-line-code-padding: 10px;
647 @cb-line-code-padding: 10px;
648 @cb-text-padding: 5px;
648 @cb-text-padding: 5px;
649
649
650 @pill-padding: 2px 7px;
650 @pill-padding: 2px 7px;
651 @pill-padding-small: 2px 2px 1px 2px;
651 @pill-padding-small: 2px 2px 1px 2px;
652
652
653 input.filediff-collapse-state {
653 input.filediff-collapse-state {
654 display: none;
654 display: none;
655
655
656 &:checked + .filediff { /* file diff is collapsed */
656 &:checked + .filediff { /* file diff is collapsed */
657 .cb {
657 .cb {
658 display: none
658 display: none
659 }
659 }
660 .filediff-collapse-indicator {
660 .filediff-collapse-indicator {
661 width: 0;
661 width: 0;
662 height: 0;
662 height: 0;
663 border-style: solid;
663 border-style: solid;
664 border-width: 4.5px 0 4.5px 9.3px;
664 border-width: 4.5px 0 4.5px 9.3px;
665 border-color: transparent transparent transparent #aaa;
665 border-color: transparent transparent transparent #aaa;
666 margin: 6px 0px;
666 margin: 6px 0px;
667 }
667 }
668 .filediff-menu {
668 .filediff-menu {
669 display: none;
669 display: none;
670 }
670 }
671
671
672 }
672 }
673
673
674 &+ .filediff { /* file diff is expanded */
674 &+ .filediff { /* file diff is expanded */
675 .filediff-collapse-indicator {
675 .filediff-collapse-indicator {
676 width: 0;
676 width: 0;
677 height: 0;
677 height: 0;
678 border-style: solid;
678 border-style: solid;
679 border-width: 9.3px 4.5px 0 4.5px;
679 border-width: 9.3px 4.5px 0 4.5px;
680 border-color: #aaa transparent transparent transparent;
680 border-color: #aaa transparent transparent transparent;
681 margin: 6px 0px;
681 margin: 6px 0px;
682
682
683 }
683 }
684 .filediff-menu {
684 .filediff-menu {
685 display: block;
685 display: block;
686 }
686 }
687 margin: 10px 0;
687 margin: 10px 0;
688 &:nth-child(2) {
688 &:nth-child(2) {
689 margin: 0;
689 margin: 0;
690 }
690 }
691 }
691 }
692 }
692 }
693
693
694 .filediffs .anchor {
694 .filediffs .anchor {
695 display: block;
695 display: block;
696 height: 40px;
696 height: 40px;
697 margin-top: -40px;
697 margin-top: -40px;
698 visibility: hidden;
698 visibility: hidden;
699 }
699 }
700
700
701 .filediffs .anchor:nth-of-type(1) {
701 .filediffs .anchor:nth-of-type(1) {
702 display: block;
702 display: block;
703 height: 80px;
703 height: 80px;
704 margin-top: -80px;
704 margin-top: -80px;
705 visibility: hidden;
705 visibility: hidden;
706 }
706 }
707
707
708 .cs_files {
708 .cs_files {
709 clear: both;
709 clear: both;
710 }
710 }
711
711
712 #diff-file-sticky{
712 #diff-file-sticky{
713 will-change: min-height;
713 will-change: min-height;
714 }
714 }
715
715
716 .sidebar__inner{
716 .sidebar__inner{
717 transform: translate(0, 0); /* For browsers don't support translate3d. */
717 transform: translate(0, 0); /* For browsers don't support translate3d. */
718 transform: translate3d(0, 0, 0);
718 transform: translate3d(0, 0, 0);
719 will-change: position, transform;
719 will-change: position, transform;
720 height: 70px;
720 height: 70px;
721 z-index: 30;
721 z-index: 30;
722 background-color: #fff;
722 background-color: #fff;
723 padding: 5px 0px;
723 padding: 5px 0px;
724 }
724 }
725
725
726 .sidebar__bar {
726 .sidebar__bar {
727 padding: 5px 0px 0px 0px
727 padding: 5px 0px 0px 0px
728 }
728 }
729
729
730 .fpath-placeholder {
730 .fpath-placeholder {
731 clear: both;
731 clear: both;
732 visibility: hidden
732 visibility: hidden
733 }
733 }
734
734
735 .is-affixed {
735 .is-affixed {
736 .sidebar_inner_shadow {
736 .sidebar_inner_shadow {
737 position: fixed;
737 position: fixed;
738 top: 75px;
738 top: 75px;
739 right: -100%;
739 right: -100%;
740 left: -100%;
740 left: -100%;
741 z-index: 28;
741 z-index: 28;
742 display: block;
742 display: block;
743 height: 5px;
743 height: 5px;
744 content: "";
744 content: "";
745 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
745 background: linear-gradient(rgba(0, 0, 0, 0.075), rgba(0, 0, 0, 0.001)) repeat-x 0 0;
746 border-top: 1px solid rgba(0, 0, 0, 0.15);
746 border-top: 1px solid rgba(0, 0, 0, 0.15);
747 }
747 }
748 .fpath-placeholder {
748 .fpath-placeholder {
749 visibility: visible !important;
749 visibility: visible !important;
750 }
750 }
751 }
751 }
752
752
753 .diffset-menu {
753 .diffset-menu {
754 margin-bottom: 20px;
754 margin-bottom: 20px;
755 }
755 }
756 .diffset {
756 .diffset {
757 margin: 20px auto;
757 margin: 20px auto;
758 .diffset-heading {
758 .diffset-heading {
759 border: 1px solid @grey5;
759 border: 1px solid @grey5;
760 margin-bottom: -1px;
760 margin-bottom: -1px;
761 // margin-top: 20px;
761 // margin-top: 20px;
762 h2 {
762 h2 {
763 margin: 0;
763 margin: 0;
764 line-height: 38px;
764 line-height: 38px;
765 padding-left: 10px;
765 padding-left: 10px;
766 }
766 }
767 .btn {
767 .btn {
768 margin: 0;
768 margin: 0;
769 }
769 }
770 background: @grey6;
770 background: @grey6;
771 display: block;
771 display: block;
772 padding: 5px;
772 padding: 5px;
773 }
773 }
774 .diffset-heading-warning {
774 .diffset-heading-warning {
775 background: @alert3-inner;
775 background: @alert3-inner;
776 border: 1px solid @alert3;
776 border: 1px solid @alert3;
777 }
777 }
778 &.diffset-comments-disabled {
778 &.diffset-comments-disabled {
779 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
779 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
780 display: none !important;
780 display: none !important;
781 }
781 }
782 }
782 }
783 }
783 }
784
784
785 .filelist {
785 .filelist {
786 .pill {
786 .pill {
787 display: block;
787 display: block;
788 float: left;
788 float: left;
789 padding: @pill-padding-small;
789 padding: @pill-padding-small;
790 }
790 }
791 }
791 }
792
792
793 .pill {
793 .pill {
794 display: block;
794 display: block;
795 float: left;
795 float: left;
796 padding: @pill-padding;
796 padding: @pill-padding;
797 }
797 }
798
798
799 .pill-group {
799 .pill-group {
800 .pill {
800 .pill {
801 opacity: .8;
801 opacity: .8;
802 margin-right: 3px;
802 margin-right: 3px;
803
803
804 &:first-child {
804 &:first-child {
805 border-radius: @border-radius 0 0 @border-radius;
805 border-radius: @border-radius 0 0 @border-radius;
806 }
806 }
807 &:last-child {
807 &:last-child {
808 border-radius: 0 @border-radius @border-radius 0;
808 border-radius: 0 @border-radius @border-radius 0;
809 }
809 }
810 &:only-child {
810 &:only-child {
811 border-radius: @border-radius;
811 border-radius: @border-radius;
812 margin-right: 0;
812 margin-right: 0;
813 }
813 }
814 }
814 }
815 }
815 }
816
816
817 /* Main comments*/
817 /* Main comments*/
818 #comments {
818 #comments {
819 .comment-selected {
819 .comment-selected {
820 border-left: 6px solid @comment-highlight-color;
820 border-left: 6px solid @comment-highlight-color;
821 padding-left: 3px;
821 padding-left: 3px;
822 margin-left: -9px;
822 margin-left: -9px;
823 }
823 }
824 }
824 }
825
825
826 .filediff {
826 .filediff {
827 border: 1px solid @grey5;
827 border: 1px solid @grey5;
828
828
829 /* START OVERRIDES */
829 /* START OVERRIDES */
830 .code-highlight {
830 .code-highlight {
831 border: none; // TODO: remove this border from the global
831 border: none; // TODO: remove this border from the global
832 // .code-highlight, it doesn't belong there
832 // .code-highlight, it doesn't belong there
833 }
833 }
834 label {
834 label {
835 margin: 0; // TODO: remove this margin definition from global label
835 margin: 0; // TODO: remove this margin definition from global label
836 // it doesn't belong there - if margin on labels
836 // it doesn't belong there - if margin on labels
837 // are needed for a form they should be defined
837 // are needed for a form they should be defined
838 // in the form's class
838 // in the form's class
839 }
839 }
840 /* END OVERRIDES */
840 /* END OVERRIDES */
841
841
842 * {
842 * {
843 box-sizing: border-box;
843 box-sizing: border-box;
844 }
844 }
845 .filediff-anchor {
845 .filediff-anchor {
846 visibility: hidden;
846 visibility: hidden;
847 }
847 }
848 &:hover {
848 &:hover {
849 .filediff-anchor {
849 .filediff-anchor {
850 visibility: visible;
850 visibility: visible;
851 }
851 }
852 }
852 }
853
853
854 .filediff-collapse-indicator {
854 .filediff-collapse-indicator {
855 border-style: solid;
855 border-style: solid;
856 float: left;
856 float: left;
857 margin: 4px 0px 0 0;
857 margin: 4px 0px 0 0;
858 cursor: pointer;
858 cursor: pointer;
859 }
859 }
860
860
861 .filediff-heading {
861 .filediff-heading {
862 background: @grey7;
862 background: @grey7;
863 cursor: pointer;
863 cursor: pointer;
864 display: block;
864 display: block;
865 padding: 5px 10px;
865 padding: 5px 10px;
866 }
866 }
867 .filediff-heading:after {
867 .filediff-heading:after {
868 content: "";
868 content: "";
869 display: table;
869 display: table;
870 clear: both;
870 clear: both;
871 }
871 }
872 .filediff-heading:hover {
872 .filediff-heading:hover {
873 background: #e1e9f4 !important;
873 background: #e1e9f4 !important;
874 }
874 }
875
875
876 .filediff-menu {
876 .filediff-menu {
877 float: right;
877 float: right;
878 text-align: right;
878 text-align: right;
879 padding: 5px 5px 5px 0px;
879 padding: 5px 5px 5px 0px;
880
880
881 &> a,
881 &> a,
882 &> span {
882 &> span {
883 padding: 1px;
883 padding: 1px;
884 }
884 }
885 }
885 }
886
886
887 .filediff-collapse-button, .filediff-expand-button {
887 .filediff-collapse-button, .filediff-expand-button {
888 cursor: pointer;
888 cursor: pointer;
889 }
889 }
890 .filediff-collapse-button {
890 .filediff-collapse-button {
891 display: inline;
891 display: inline;
892 }
892 }
893 .filediff-expand-button {
893 .filediff-expand-button {
894 display: none;
894 display: none;
895 }
895 }
896 .filediff-collapsed .filediff-collapse-button {
896 .filediff-collapsed .filediff-collapse-button {
897 display: none;
897 display: none;
898 }
898 }
899 .filediff-collapsed .filediff-expand-button {
899 .filediff-collapsed .filediff-expand-button {
900 display: inline;
900 display: inline;
901 }
901 }
902
902
903 /**** COMMENTS ****/
903 /**** COMMENTS ****/
904
904
905 .filediff-menu {
905 .filediff-menu {
906 .show-comment-button {
906 .show-comment-button {
907 display: none;
907 display: none;
908 }
908 }
909 }
909 }
910 &.hide-comments {
910 &.hide-comments {
911 .inline-comments {
911 .inline-comments {
912 display: none;
912 display: none;
913 }
913 }
914 .filediff-menu {
914 .filediff-menu {
915 .show-comment-button {
915 .show-comment-button {
916 display: inline;
916 display: inline;
917 }
917 }
918 .hide-comment-button {
918 .hide-comment-button {
919 display: none;
919 display: none;
920 }
920 }
921 }
921 }
922 }
922 }
923
923
924 .hide-line-comments {
924 .hide-line-comments {
925 .inline-comments {
925 .inline-comments {
926 display: none;
926 display: none;
927 }
927 }
928 }
928 }
929
929
930 /**** END COMMENTS ****/
930 /**** END COMMENTS ****/
931
931
932 }
932 }
933
933
934
934
935
935
936 .filediff, .filelist {
936 .filediff, .filelist {
937 .pill {
937 .pill {
938 &[op="name"] {
938 &[op="name"] {
939 background: none;
939 background: none;
940 opacity: 1;
940 opacity: 1;
941 color: white;
941 color: white;
942 }
942 }
943 &[op="limited"] {
943 &[op="limited"] {
944 background: @grey2;
944 background: @grey2;
945 color: white;
945 color: white;
946 }
946 }
947 &[op="binary"] {
947 &[op="binary"] {
948 background: @color7;
948 background: @color7;
949 color: white;
949 color: white;
950 }
950 }
951 &[op="modified"] {
951 &[op="modified"] {
952 background: @alert1;
952 background: @alert1;
953 color: white;
953 color: white;
954 }
954 }
955 &[op="renamed"] {
955 &[op="renamed"] {
956 background: @color4;
956 background: @color4;
957 color: white;
957 color: white;
958 }
958 }
959 &[op="copied"] {
959 &[op="copied"] {
960 background: @color4;
960 background: @color4;
961 color: white;
961 color: white;
962 }
962 }
963 &[op="mode"] {
963 &[op="mode"] {
964 background: @grey3;
964 background: @grey3;
965 color: white;
965 color: white;
966 }
966 }
967 &[op="symlink"] {
967 &[op="symlink"] {
968 background: @color8;
968 background: @color8;
969 color: white;
969 color: white;
970 }
970 }
971
971
972 &[op="added"] { /* added lines */
972 &[op="added"] { /* added lines */
973 background: @alert1;
973 background: @alert1;
974 color: white;
974 color: white;
975 }
975 }
976 &[op="deleted"] { /* deleted lines */
976 &[op="deleted"] { /* deleted lines */
977 background: @alert2;
977 background: @alert2;
978 color: white;
978 color: white;
979 }
979 }
980
980
981 &[op="created"] { /* created file */
981 &[op="created"] { /* created file */
982 background: @alert1;
982 background: @alert1;
983 color: white;
983 color: white;
984 }
984 }
985 &[op="removed"] { /* deleted file */
985 &[op="removed"] { /* deleted file */
986 background: @color5;
986 background: @color5;
987 color: white;
987 color: white;
988 }
988 }
989 }
989 }
990 }
990 }
991
991
992
992
993 .filediff-outdated {
993 .filediff-outdated {
994 padding: 8px 0;
994 padding: 8px 0;
995
995
996 .filediff-heading {
996 .filediff-heading {
997 opacity: .5;
997 opacity: .5;
998 }
998 }
999 }
999 }
1000
1000
1001 table.cb {
1001 table.cb {
1002 width: 100%;
1002 width: 100%;
1003 border-collapse: collapse;
1003 border-collapse: collapse;
1004
1004
1005 .cb-text {
1005 .cb-text {
1006 padding: @cb-text-padding;
1006 padding: @cb-text-padding;
1007 }
1007 }
1008 .cb-hunk {
1008 .cb-hunk {
1009 padding: @cb-text-padding;
1009 padding: @cb-text-padding;
1010 }
1010 }
1011 .cb-expand {
1011 .cb-expand {
1012 display: none;
1012 display: none;
1013 }
1013 }
1014 .cb-collapse {
1014 .cb-collapse {
1015 display: inline;
1015 display: inline;
1016 }
1016 }
1017 &.cb-collapsed {
1017 &.cb-collapsed {
1018 .cb-line {
1018 .cb-line {
1019 display: none;
1019 display: none;
1020 }
1020 }
1021 .cb-expand {
1021 .cb-expand {
1022 display: inline;
1022 display: inline;
1023 }
1023 }
1024 .cb-collapse {
1024 .cb-collapse {
1025 display: none;
1025 display: none;
1026 }
1026 }
1027 }
1027 }
1028
1028
1029 /* intentionally general selector since .cb-line-selected must override it
1029 /* intentionally general selector since .cb-line-selected must override it
1030 and they both use !important since the td itself may have a random color
1030 and they both use !important since the td itself may have a random color
1031 generated by annotation blocks. TLDR: if you change it, make sure
1031 generated by annotation blocks. TLDR: if you change it, make sure
1032 annotated block selection and line selection in file view still work */
1032 annotated block selection and line selection in file view still work */
1033 .cb-line-fresh .cb-content {
1033 .cb-line-fresh .cb-content {
1034 background: white !important;
1034 background: white !important;
1035 }
1035 }
1036 .cb-warning {
1036 .cb-warning {
1037 background: #fff4dd;
1037 background: #fff4dd;
1038 }
1038 }
1039
1039
1040 &.cb-diff-sideside {
1040 &.cb-diff-sideside {
1041 td {
1041 td {
1042 &.cb-content {
1042 &.cb-content {
1043 width: 50%;
1043 width: 50%;
1044 }
1044 }
1045 }
1045 }
1046 }
1046 }
1047
1047
1048 tr {
1048 tr {
1049 &.cb-annotate {
1049 &.cb-annotate {
1050 border-top: 1px solid #eee;
1050 border-top: 1px solid #eee;
1051 }
1051 }
1052
1052
1053 &.cb-comment-info {
1053 &.cb-comment-info {
1054 border-top: 1px solid #eee;
1054 border-top: 1px solid #eee;
1055 color: rgba(0, 0, 0, 0.3);
1055 color: rgba(0, 0, 0, 0.3);
1056 background: #edf2f9;
1056 background: #edf2f9;
1057
1057
1058 td {
1058 td {
1059
1059
1060 }
1060 }
1061 }
1061 }
1062
1062
1063 &.cb-hunk {
1063 &.cb-hunk {
1064 font-family: @font-family-monospace;
1064 font-family: @font-family-monospace;
1065 color: rgba(0, 0, 0, 0.3);
1065 color: rgba(0, 0, 0, 0.3);
1066
1066
1067 td {
1067 td {
1068 &:first-child {
1068 &:first-child {
1069 background: #edf2f9;
1069 background: #edf2f9;
1070 }
1070 }
1071 &:last-child {
1071 &:last-child {
1072 background: #f4f7fb;
1072 background: #f4f7fb;
1073 }
1073 }
1074 }
1074 }
1075 }
1075 }
1076 }
1076 }
1077
1077
1078
1078
1079 td {
1079 td {
1080 vertical-align: top;
1080 vertical-align: top;
1081 padding: 0;
1081 padding: 0;
1082
1082
1083 &.cb-content {
1083 &.cb-content {
1084 font-size: 12.35px;
1084 font-size: 12.35px;
1085
1085
1086 &.cb-line-selected .cb-code {
1086 &.cb-line-selected .cb-code {
1087 background: @comment-highlight-color !important;
1087 background: @comment-highlight-color !important;
1088 }
1088 }
1089
1089
1090 span.cb-code {
1090 span.cb-code {
1091 line-height: @cb-line-height;
1091 line-height: @cb-line-height;
1092 padding-left: @cb-line-code-padding;
1092 padding-left: @cb-line-code-padding;
1093 padding-right: @cb-line-code-padding;
1093 padding-right: @cb-line-code-padding;
1094 display: block;
1094 display: block;
1095 white-space: pre-wrap;
1095 white-space: pre-wrap;
1096 font-family: @font-family-monospace;
1096 font-family: @font-family-monospace;
1097 word-break: break-all;
1097 word-break: break-all;
1098 .nonl {
1098 .nonl {
1099 color: @color5;
1099 color: @color5;
1100 }
1100 }
1101 .cb-action {
1102 &:before {
1103 content: " ";
1104 }
1105 &.cb-deletion:before {
1106 content: "- ";
1107 }
1108 &.cb-addition:before {
1109 content: "+ ";
1110 }
1111 }
1101 }
1112 }
1102
1113
1103 &> button.cb-comment-box-opener {
1114 &> button.cb-comment-box-opener {
1104
1115
1105 padding: 2px 2px 1px 3px;
1116 padding: 2px 2px 1px 3px;
1106 margin-left: -6px;
1117 margin-left: -6px;
1107 margin-top: -1px;
1118 margin-top: -1px;
1108
1119
1109 border-radius: @border-radius;
1120 border-radius: @border-radius;
1110 position: absolute;
1121 position: absolute;
1111 display: none;
1122 display: none;
1112 }
1123 }
1113 .cb-comment {
1124 .cb-comment {
1114 margin-top: 10px;
1125 margin-top: 10px;
1115 white-space: normal;
1126 white-space: normal;
1116 }
1127 }
1117 }
1128 }
1118 &:hover {
1129 &:hover {
1119 button.cb-comment-box-opener {
1130 button.cb-comment-box-opener {
1120 display: block;
1131 display: block;
1121 }
1132 }
1122 &+ td button.cb-comment-box-opener {
1133 &+ td button.cb-comment-box-opener {
1123 display: block
1134 display: block
1124 }
1135 }
1125 }
1136 }
1126
1137
1127 &.cb-data {
1138 &.cb-data {
1128 text-align: right;
1139 text-align: right;
1129 width: 30px;
1140 width: 30px;
1130 font-family: @font-family-monospace;
1141 font-family: @font-family-monospace;
1131
1142
1132 .icon-comment {
1143 .icon-comment {
1133 cursor: pointer;
1144 cursor: pointer;
1134 }
1145 }
1135 &.cb-line-selected {
1146 &.cb-line-selected {
1136 background: @comment-highlight-color !important;
1147 background: @comment-highlight-color !important;
1137 }
1148 }
1138 &.cb-line-selected > div {
1149 &.cb-line-selected > div {
1139 display: block;
1150 display: block;
1140 background: @comment-highlight-color !important;
1151 background: @comment-highlight-color !important;
1141 line-height: @cb-line-height;
1152 line-height: @cb-line-height;
1142 color: rgba(0, 0, 0, 0.3);
1153 color: rgba(0, 0, 0, 0.3);
1143 }
1154 }
1144 }
1155 }
1145
1156
1146 &.cb-lineno {
1157 &.cb-lineno {
1147 padding: 0;
1158 padding: 0;
1148 width: 50px;
1159 width: 50px;
1149 color: rgba(0, 0, 0, 0.3);
1160 color: rgba(0, 0, 0, 0.3);
1150 text-align: right;
1161 text-align: right;
1151 border-right: 1px solid #eee;
1162 border-right: 1px solid #eee;
1152 font-family: @font-family-monospace;
1163 font-family: @font-family-monospace;
1153 -webkit-user-select: none;
1164 -webkit-user-select: none;
1154 -moz-user-select: none;
1165 -moz-user-select: none;
1155 user-select: none;
1166 user-select: none;
1156
1167
1157 a::before {
1168 a::before {
1158 content: attr(data-line-no);
1169 content: attr(data-line-no);
1159 }
1170 }
1160 &.cb-line-selected {
1171 &.cb-line-selected {
1161 background: @comment-highlight-color !important;
1172 background: @comment-highlight-color !important;
1162 }
1173 }
1163
1174
1164 a {
1175 a {
1165 display: block;
1176 display: block;
1166 padding-right: @cb-line-code-padding;
1177 padding-right: @cb-line-code-padding;
1167 padding-left: @cb-line-code-padding;
1178 padding-left: @cb-line-code-padding;
1168 line-height: @cb-line-height;
1179 line-height: @cb-line-height;
1169 color: rgba(0, 0, 0, 0.3);
1180 color: rgba(0, 0, 0, 0.3);
1170 }
1181 }
1171 }
1182 }
1172
1183
1173 &.cb-empty {
1184 &.cb-empty {
1174 background: @grey7;
1185 background: @grey7;
1175 }
1186 }
1176
1187
1177 ins {
1188 ins {
1178 color: black;
1189 color: black;
1179 background: #a6f3a6;
1190 background: #a6f3a6;
1180 text-decoration: none;
1191 text-decoration: none;
1181 }
1192 }
1182 del {
1193 del {
1183 color: black;
1194 color: black;
1184 background: #f8cbcb;
1195 background: #f8cbcb;
1185 text-decoration: none;
1196 text-decoration: none;
1186 }
1197 }
1187 &.cb-addition {
1198 &.cb-addition {
1188 background: #ecffec;
1199 background: #ecffec;
1189
1200
1190 &.blob-lineno {
1201 &.blob-lineno {
1191 background: #ddffdd;
1202 background: #ddffdd;
1192 }
1203 }
1193 }
1204 }
1194 &.cb-deletion {
1205 &.cb-deletion {
1195 background: #ffecec;
1206 background: #ffecec;
1196
1207
1197 &.blob-lineno {
1208 &.blob-lineno {
1198 background: #ffdddd;
1209 background: #ffdddd;
1199 }
1210 }
1200 }
1211 }
1201 &.cb-annotate-message-spacer {
1212 &.cb-annotate-message-spacer {
1202 width:8px;
1213 width:8px;
1203 padding: 1px 0px 0px 3px;
1214 padding: 1px 0px 0px 3px;
1204 }
1215 }
1205 &.cb-annotate-info {
1216 &.cb-annotate-info {
1206 width: 320px;
1217 width: 320px;
1207 min-width: 320px;
1218 min-width: 320px;
1208 max-width: 320px;
1219 max-width: 320px;
1209 padding: 5px 2px;
1220 padding: 5px 2px;
1210 font-size: 13px;
1221 font-size: 13px;
1211
1222
1212 .cb-annotate-message {
1223 .cb-annotate-message {
1213 padding: 2px 0px 0px 0px;
1224 padding: 2px 0px 0px 0px;
1214 white-space: pre-line;
1225 white-space: pre-line;
1215 overflow: hidden;
1226 overflow: hidden;
1216 }
1227 }
1217 .rc-user {
1228 .rc-user {
1218 float: none;
1229 float: none;
1219 padding: 0 6px 0 17px;
1230 padding: 0 6px 0 17px;
1220 min-width: unset;
1231 min-width: unset;
1221 min-height: unset;
1232 min-height: unset;
1222 }
1233 }
1223 }
1234 }
1224
1235
1225 &.cb-annotate-revision {
1236 &.cb-annotate-revision {
1226 cursor: pointer;
1237 cursor: pointer;
1227 text-align: right;
1238 text-align: right;
1228 padding: 1px 3px 0px 3px;
1239 padding: 1px 3px 0px 3px;
1229 }
1240 }
1230 }
1241 }
1231 }
1242 }
@@ -1,1016 +1,1016 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
47 deleted_files_comments=None,
48
48
49 # for cache purpose
49 # for cache purpose
50 inline_comments=None,
50 inline_comments=None,
51
51
52 )">
52 )">
53 %if use_comments:
53 %if use_comments:
54 <div id="cb-comments-inline-container-template" class="js-template">
54 <div id="cb-comments-inline-container-template" class="js-template">
55 ${inline_comments_container([], inline_comments)}
55 ${inline_comments_container([], inline_comments)}
56 </div>
56 </div>
57 <div class="js-template" id="cb-comment-inline-form-template">
57 <div class="js-template" id="cb-comment-inline-form-template">
58 <div class="comment-inline-form ac">
58 <div class="comment-inline-form ac">
59
59
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 ## render template for inline comments
61 ## render template for inline comments
62 ${commentblock.comment_form(form_type='inline')}
62 ${commentblock.comment_form(form_type='inline')}
63 %else:
63 %else:
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 <div class="pull-left">
65 <div class="pull-left">
66 <div class="comment-help pull-right">
66 <div class="comment-help pull-right">
67 ${_('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>
67 ${_('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>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button pull-right">
70 <div class="comment-button pull-right">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 ${_('Cancel')}
72 ${_('Cancel')}
73 </button>
73 </button>
74 </div>
74 </div>
75 <div class="clearfix"></div>
75 <div class="clearfix"></div>
76 ${h.end_form()}
76 ${h.end_form()}
77 %endif
77 %endif
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 %endif
81 %endif
82 <%
82 <%
83 collapse_all = len(diffset.files) > collapse_when_files_over
83 collapse_all = len(diffset.files) > collapse_when_files_over
84 %>
84 %>
85
85
86 %if c.user_session_attrs["diffmode"] == 'sideside':
86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 <style>
87 <style>
88 .wrapper {
88 .wrapper {
89 max-width: 1600px !important;
89 max-width: 1600px !important;
90 }
90 }
91 </style>
91 </style>
92 %endif
92 %endif
93
93
94 %if ruler_at_chars:
94 %if ruler_at_chars:
95 <style>
95 <style>
96 .diff table.cb .cb-content:after {
96 .diff table.cb .cb-content:after {
97 content: "";
97 content: "";
98 border-left: 1px solid blue;
98 border-left: 1px solid blue;
99 position: absolute;
99 position: absolute;
100 top: 0;
100 top: 0;
101 height: 18px;
101 height: 18px;
102 opacity: .2;
102 opacity: .2;
103 z-index: 10;
103 z-index: 10;
104 //## +5 to account for diff action (+/-)
104 //## +5 to account for diff action (+/-)
105 left: ${ruler_at_chars + 5}ch;
105 left: ${ruler_at_chars + 5}ch;
106 </style>
106 </style>
107 %endif
107 %endif
108
108
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 %if commit:
111 %if commit:
112 <div class="pull-right">
112 <div class="pull-right">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 ${_('Browse Files')}
114 ${_('Browse Files')}
115 </a>
115 </a>
116 </div>
116 </div>
117 %endif
117 %endif
118 <h2 class="clearinner">
118 <h2 class="clearinner">
119 ## invidual commit
119 ## invidual commit
120 % if commit:
120 % if commit:
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 ${h.age_component(commit.date)}
122 ${h.age_component(commit.date)}
123 % if diffset.limited_diff:
123 % if diffset.limited_diff:
124 - ${_('The requested commit is too big and content was truncated.')}
124 - ${_('The requested commit is too big and content was truncated.')}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 <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>
126 <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>
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 ## compare diff, has no file-selector and we want to show stats anyway
128 ## compare diff, has no file-selector and we want to show stats anyway
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 % endif
132 % endif
133 % else:
133 % else:
134 ## pull requests/compare
134 ## pull requests/compare
135 ${_('File Changes')}
135 ${_('File Changes')}
136 % endif
136 % endif
137
137
138 </h2>
138 </h2>
139 </div>
139 </div>
140
140
141 %if diffset.has_hidden_changes:
141 %if diffset.has_hidden_changes:
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 %elif not diffset.files:
143 %elif not diffset.files:
144 <p class="empty_data">${_('No files')}</p>
144 <p class="empty_data">${_('No files')}</p>
145 %endif
145 %endif
146
146
147 <div class="filediffs">
147 <div class="filediffs">
148
148
149 ## initial value could be marked as False later on
149 ## initial value could be marked as False later on
150 <% over_lines_changed_limit = False %>
150 <% over_lines_changed_limit = False %>
151 %for i, filediff in enumerate(diffset.files):
151 %for i, filediff in enumerate(diffset.files):
152
152
153 <%
153 <%
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 over_lines_changed_limit = lines_changed > lines_changed_limit
155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 %>
156 %>
157 ## anchor with support of sticky header
157 ## anchor with support of sticky header
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159
159
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 <div
161 <div
162 class="filediff"
162 class="filediff"
163 data-f-path="${filediff.patch['filename']}"
163 data-f-path="${filediff.patch['filename']}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 >
165 >
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 <div class="filediff-collapse-indicator"></div>
167 <div class="filediff-collapse-indicator"></div>
168 ${diff_ops(filediff)}
168 ${diff_ops(filediff)}
169 </label>
169 </label>
170
170
171 ${diff_menu(filediff, use_comments=use_comments)}
171 ${diff_menu(filediff, use_comments=use_comments)}
172 <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 '')}">
172 <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 '')}">
173
173
174 ## new/deleted/empty content case
174 ## new/deleted/empty content case
175 % if not filediff.hunks:
175 % if not filediff.hunks:
176 ## Comment container, on "fakes" hunk that contains all data to render comments
176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 % endif
178 % endif
179
179
180 %if filediff.limited_diff:
180 %if filediff.limited_diff:
181 <tr class="cb-warning cb-collapser">
181 <tr class="cb-warning cb-collapser">
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 ${_('The requested commit 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>
183 ${_('The requested commit 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>
184 </td>
184 </td>
185 </tr>
185 </tr>
186 %else:
186 %else:
187 %if over_lines_changed_limit:
187 %if over_lines_changed_limit:
188 <tr class="cb-warning cb-collapser">
188 <tr class="cb-warning cb-collapser">
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 <a href="#" class="cb-expand"
191 <a href="#" class="cb-expand"
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 </a>
193 </a>
194 <a href="#" class="cb-collapse"
194 <a href="#" class="cb-collapse"
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 </a>
196 </a>
197 </td>
197 </td>
198 </tr>
198 </tr>
199 %endif
199 %endif
200 %endif
200 %endif
201
201
202 % for hunk in filediff.hunks:
202 % for hunk in filediff.hunks:
203 <tr class="cb-hunk">
203 <tr class="cb-hunk">
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 ## TODO: dan: add ajax loading of more context here
205 ## TODO: dan: add ajax loading of more context here
206 ## <a href="#">
206 ## <a href="#">
207 <i class="icon-more"></i>
207 <i class="icon-more"></i>
208 ## </a>
208 ## </a>
209 </td>
209 </td>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 @@
211 @@
212 -${hunk.source_start},${hunk.source_length}
212 -${hunk.source_start},${hunk.source_length}
213 +${hunk.target_start},${hunk.target_length}
213 +${hunk.target_start},${hunk.target_length}
214 ${hunk.section_header}
214 ${hunk.section_header}
215 </td>
215 </td>
216 </tr>
216 </tr>
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 % endfor
218 % endfor
219
219
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221
221
222 ## outdated comments that do not fit into currently displayed lines
222 ## outdated comments that do not fit into currently displayed lines
223 % for lineno, comments in unmatched_comments.items():
223 % for lineno, comments in unmatched_comments.items():
224
224
225 %if c.user_session_attrs["diffmode"] == 'unified':
225 %if c.user_session_attrs["diffmode"] == 'unified':
226 % if loop.index == 0:
226 % if loop.index == 0:
227 <tr class="cb-hunk">
227 <tr class="cb-hunk">
228 <td colspan="3"></td>
228 <td colspan="3"></td>
229 <td>
229 <td>
230 <div>
230 <div>
231 ${_('Unmatched inline comments below')}
231 ${_('Unmatched inline comments below')}
232 </div>
232 </div>
233 </td>
233 </td>
234 </tr>
234 </tr>
235 % endif
235 % endif
236 <tr class="cb-line">
236 <tr class="cb-line">
237 <td class="cb-data cb-context"></td>
237 <td class="cb-data cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
240 <td class="cb-content cb-context">
240 <td class="cb-content cb-context">
241 ${inline_comments_container(comments, inline_comments)}
241 ${inline_comments_container(comments, inline_comments)}
242 </td>
242 </td>
243 </tr>
243 </tr>
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 % if loop.index == 0:
245 % if loop.index == 0:
246 <tr class="cb-comment-info">
246 <tr class="cb-comment-info">
247 <td colspan="2"></td>
247 <td colspan="2"></td>
248 <td class="cb-line">
248 <td class="cb-line">
249 <div>
249 <div>
250 ${_('Unmatched inline comments below')}
250 ${_('Unmatched inline comments below')}
251 </div>
251 </div>
252 </td>
252 </td>
253 <td colspan="2"></td>
253 <td colspan="2"></td>
254 <td class="cb-line">
254 <td class="cb-line">
255 <div>
255 <div>
256 ${_('Unmatched comments below')}
256 ${_('Unmatched comments below')}
257 </div>
257 </div>
258 </td>
258 </td>
259 </tr>
259 </tr>
260 % endif
260 % endif
261 <tr class="cb-line">
261 <tr class="cb-line">
262 <td class="cb-data cb-context"></td>
262 <td class="cb-data cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
264 <td class="cb-content cb-context">
264 <td class="cb-content cb-context">
265 % if lineno.startswith('o'):
265 % if lineno.startswith('o'):
266 ${inline_comments_container(comments, inline_comments)}
266 ${inline_comments_container(comments, inline_comments)}
267 % endif
267 % endif
268 </td>
268 </td>
269
269
270 <td class="cb-data cb-context"></td>
270 <td class="cb-data cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
272 <td class="cb-content cb-context">
272 <td class="cb-content cb-context">
273 % if lineno.startswith('n'):
273 % if lineno.startswith('n'):
274 ${inline_comments_container(comments, inline_comments)}
274 ${inline_comments_container(comments, inline_comments)}
275 % endif
275 % endif
276 </td>
276 </td>
277 </tr>
277 </tr>
278 %endif
278 %endif
279
279
280 % endfor
280 % endfor
281
281
282 </table>
282 </table>
283 </div>
283 </div>
284 %endfor
284 %endfor
285
285
286 ## outdated comments that are made for a file that has been deleted
286 ## outdated comments that are made for a file that has been deleted
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 <%
288 <%
289 display_state = 'display: none'
289 display_state = 'display: none'
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 if open_comments_in_file:
291 if open_comments_in_file:
292 display_state = ''
292 display_state = ''
293 %>
293 %>
294 <div class="filediffs filediff-outdated" style="${display_state}">
294 <div class="filediffs filediff-outdated" style="${display_state}">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 <div class="filediff-collapse-indicator"></div>
298 <div class="filediff-collapse-indicator"></div>
299 <span class="pill">
299 <span class="pill">
300 ## file was deleted
300 ## file was deleted
301 <strong>${filename}</strong>
301 <strong>${filename}</strong>
302 </span>
302 </span>
303 <span class="pill-group" style="float: left">
303 <span class="pill-group" style="float: left">
304 ## file op, doesn't need translation
304 ## file op, doesn't need translation
305 <span class="pill" op="removed">removed in this version</span>
305 <span class="pill" op="removed">removed in this version</span>
306 </span>
306 </span>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
308 <span class="pill-group" style="float: right">
308 <span class="pill-group" style="float: right">
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 </span>
310 </span>
311 </label>
311 </label>
312
312
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 <tr>
314 <tr>
315 % if c.user_session_attrs["diffmode"] == 'unified':
315 % if c.user_session_attrs["diffmode"] == 'unified':
316 <td></td>
316 <td></td>
317 %endif
317 %endif
318
318
319 <td></td>
319 <td></td>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 </td>
322 </td>
323 </tr>
323 </tr>
324 %if c.user_session_attrs["diffmode"] == 'unified':
324 %if c.user_session_attrs["diffmode"] == 'unified':
325 <tr class="cb-line">
325 <tr class="cb-line">
326 <td class="cb-data cb-context"></td>
326 <td class="cb-data cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
329 <td class="cb-content cb-context">
329 <td class="cb-content cb-context">
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 </td>
331 </td>
332 </tr>
332 </tr>
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 <tr class="cb-line">
334 <tr class="cb-line">
335 <td class="cb-data cb-context"></td>
335 <td class="cb-data cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
337 <td class="cb-content cb-context"></td>
337 <td class="cb-content cb-context"></td>
338
338
339 <td class="cb-data cb-context"></td>
339 <td class="cb-data cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
341 <td class="cb-content cb-context">
341 <td class="cb-content cb-context">
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 </td>
343 </td>
344 </tr>
344 </tr>
345 %endif
345 %endif
346 </table>
346 </table>
347 </div>
347 </div>
348 </div>
348 </div>
349 % endfor
349 % endfor
350
350
351 </div>
351 </div>
352 </div>
352 </div>
353 </%def>
353 </%def>
354
354
355 <%def name="diff_ops(filediff)">
355 <%def name="diff_ops(filediff)">
356 <%
356 <%
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 %>
359 %>
360 <span class="pill">
360 <span class="pill">
361 %if filediff.source_file_path and filediff.target_file_path:
361 %if filediff.source_file_path and filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
363 ## file was renamed, or copied
363 ## file was renamed, or copied
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
366 <% final_path = filediff.target_file_path %>
366 <% final_path = filediff.target_file_path %>
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
369 <% final_path = filediff.target_file_path %>
369 <% final_path = filediff.target_file_path %>
370 %endif
370 %endif
371 %else:
371 %else:
372 ## file was modified
372 ## file was modified
373 <strong>${filediff.source_file_path}</strong>
373 <strong>${filediff.source_file_path}</strong>
374 <% final_path = filediff.source_file_path %>
374 <% final_path = filediff.source_file_path %>
375 %endif
375 %endif
376 %else:
376 %else:
377 %if filediff.source_file_path:
377 %if filediff.source_file_path:
378 ## file was deleted
378 ## file was deleted
379 <strong>${filediff.source_file_path}</strong>
379 <strong>${filediff.source_file_path}</strong>
380 <% final_path = filediff.source_file_path %>
380 <% final_path = filediff.source_file_path %>
381 %else:
381 %else:
382 ## file was added
382 ## file was added
383 <strong>${filediff.target_file_path}</strong>
383 <strong>${filediff.target_file_path}</strong>
384 <% final_path = filediff.target_file_path %>
384 <% final_path = filediff.target_file_path %>
385 %endif
385 %endif
386 %endif
386 %endif
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 </span>
388 </span>
389 ## anchor link
389 ## anchor link
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
391
391
392 <span class="pill-group" style="float: right">
392 <span class="pill-group" style="float: right">
393
393
394 ## ops pills
394 ## ops pills
395 %if filediff.limited_diff:
395 %if filediff.limited_diff:
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 %endif
397 %endif
398
398
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 <span class="pill" op="created">created</span>
400 <span class="pill" op="created">created</span>
401 %if filediff['target_mode'].startswith('120'):
401 %if filediff['target_mode'].startswith('120'):
402 <span class="pill" op="symlink">symlink</span>
402 <span class="pill" op="symlink">symlink</span>
403 %else:
403 %else:
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 %endif
405 %endif
406 %endif
406 %endif
407
407
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 <span class="pill" op="renamed">renamed</span>
409 <span class="pill" op="renamed">renamed</span>
410 %endif
410 %endif
411
411
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 <span class="pill" op="copied">copied</span>
413 <span class="pill" op="copied">copied</span>
414 %endif
414 %endif
415
415
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 <span class="pill" op="removed">removed</span>
417 <span class="pill" op="removed">removed</span>
418 %endif
418 %endif
419
419
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 <span class="pill" op="mode">
421 <span class="pill" op="mode">
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
423 </span>
423 </span>
424 %endif
424 %endif
425
425
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 <span class="pill" op="binary">binary</span>
427 <span class="pill" op="binary">binary</span>
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 <span class="pill" op="modified">modified</span>
429 <span class="pill" op="modified">modified</span>
430 %endif
430 %endif
431 %endif
431 %endif
432
432
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435
435
436 </span>
436 </span>
437
437
438 </%def>
438 </%def>
439
439
440 <%def name="nice_mode(filemode)">
440 <%def name="nice_mode(filemode)">
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 </%def>
442 </%def>
443
443
444 <%def name="diff_menu(filediff, use_comments=False)">
444 <%def name="diff_menu(filediff, use_comments=False)">
445 <div class="filediff-menu">
445 <div class="filediff-menu">
446 %if filediff.diffset.source_ref:
446 %if filediff.diffset.source_ref:
447 %if filediff.operation in ['D', 'M']:
447 %if filediff.operation in ['D', 'M']:
448 <a
448 <a
449 class="tooltip"
449 class="tooltip"
450 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
450 href="${h.route_path('repo_files',repo_name=filediff.diffset.repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
451 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
451 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
452 >
452 >
453 ${_('Show file before')}
453 ${_('Show file before')}
454 </a> |
454 </a> |
455 %else:
455 %else:
456 <span
456 <span
457 class="tooltip"
457 class="tooltip"
458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
459 >
459 >
460 ${_('Show file before')}
460 ${_('Show file before')}
461 </span> |
461 </span> |
462 %endif
462 %endif
463 %if filediff.operation in ['A', 'M']:
463 %if filediff.operation in ['A', 'M']:
464 <a
464 <a
465 class="tooltip"
465 class="tooltip"
466 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)}"
466 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)}"
467 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
467 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
468 >
468 >
469 ${_('Show file after')}
469 ${_('Show file after')}
470 </a>
470 </a>
471 %else:
471 %else:
472 <span
472 <span
473 class="tooltip"
473 class="tooltip"
474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
475 >
475 >
476 ${_('Show file after')}
476 ${_('Show file after')}
477 </span>
477 </span>
478 %endif
478 %endif
479
479
480 % if use_comments:
480 % if use_comments:
481 |
481 |
482 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
482 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
483 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
483 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
484 </a>
484 </a>
485 % endif
485 % endif
486
486
487 %endif
487 %endif
488 </div>
488 </div>
489 </%def>
489 </%def>
490
490
491
491
492 <%def name="inline_comments_container(comments, inline_comments)">
492 <%def name="inline_comments_container(comments, inline_comments)">
493 <div class="inline-comments">
493 <div class="inline-comments">
494 %for comment in comments:
494 %for comment in comments:
495 ${commentblock.comment_block(comment, inline=True)}
495 ${commentblock.comment_block(comment, inline=True)}
496 %endfor
496 %endfor
497 % if comments and comments[-1].outdated:
497 % if comments and comments[-1].outdated:
498 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
498 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
499 style="display: none;}">
499 style="display: none;}">
500 ${_('Add another comment')}
500 ${_('Add another comment')}
501 </span>
501 </span>
502 % else:
502 % else:
503 <span onclick="return Rhodecode.comments.createComment(this)"
503 <span onclick="return Rhodecode.comments.createComment(this)"
504 class="btn btn-secondary cb-comment-add-button">
504 class="btn btn-secondary cb-comment-add-button">
505 ${_('Add another comment')}
505 ${_('Add another comment')}
506 </span>
506 </span>
507 % endif
507 % endif
508
508
509 </div>
509 </div>
510 </%def>
510 </%def>
511
511
512 <%!
512 <%!
513 def get_comments_for(diff_type, comments, filename, line_version, line_number):
513 def get_comments_for(diff_type, comments, filename, line_version, line_number):
514 if hasattr(filename, 'unicode_path'):
514 if hasattr(filename, 'unicode_path'):
515 filename = filename.unicode_path
515 filename = filename.unicode_path
516
516
517 if not isinstance(filename, basestring):
517 if not isinstance(filename, basestring):
518 return None
518 return None
519
519
520 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
520 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
521
521
522 if comments and filename in comments:
522 if comments and filename in comments:
523 file_comments = comments[filename]
523 file_comments = comments[filename]
524 if line_key in file_comments:
524 if line_key in file_comments:
525 data = file_comments.pop(line_key)
525 data = file_comments.pop(line_key)
526 return data
526 return data
527 %>
527 %>
528
528
529 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
529 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
530 %for i, line in enumerate(hunk.sideside):
530 %for i, line in enumerate(hunk.sideside):
531 <%
531 <%
532 old_line_anchor, new_line_anchor = None, None
532 old_line_anchor, new_line_anchor = None, None
533
533
534 if line.original.lineno:
534 if line.original.lineno:
535 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
535 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
536 if line.modified.lineno:
536 if line.modified.lineno:
537 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
537 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
538 %>
538 %>
539
539
540 <tr class="cb-line">
540 <tr class="cb-line">
541 <td class="cb-data ${action_class(line.original.action)}"
541 <td class="cb-data ${action_class(line.original.action)}"
542 data-line-no="${line.original.lineno}"
542 data-line-no="${line.original.lineno}"
543 >
543 >
544 <div>
544 <div>
545
545
546 <% line_old_comments = None %>
546 <% line_old_comments = None %>
547 %if line.original.get_comment_args:
547 %if line.original.get_comment_args:
548 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
548 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
549 %endif
549 %endif
550 %if line_old_comments:
550 %if line_old_comments:
551 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
551 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
552 % if has_outdated:
552 % if has_outdated:
553 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
553 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
554 % else:
554 % else:
555 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
555 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
556 % endif
556 % endif
557 %endif
557 %endif
558 </div>
558 </div>
559 </td>
559 </td>
560 <td class="cb-lineno ${action_class(line.original.action)}"
560 <td class="cb-lineno ${action_class(line.original.action)}"
561 data-line-no="${line.original.lineno}"
561 data-line-no="${line.original.lineno}"
562 %if old_line_anchor:
562 %if old_line_anchor:
563 id="${old_line_anchor}"
563 id="${old_line_anchor}"
564 %endif
564 %endif
565 >
565 >
566 %if line.original.lineno:
566 %if line.original.lineno:
567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 %endif
568 %endif
569 </td>
569 </td>
570 <td class="cb-content ${action_class(line.original.action)}"
570 <td class="cb-content ${action_class(line.original.action)}"
571 data-line-no="o${line.original.lineno}"
571 data-line-no="o${line.original.lineno}"
572 >
572 >
573 %if use_comments and line.original.lineno:
573 %if use_comments and line.original.lineno:
574 ${render_add_comment_button()}
574 ${render_add_comment_button()}
575 %endif
575 %endif
576 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
576 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
577
577
578 %if use_comments and line.original.lineno and line_old_comments:
578 %if use_comments and line.original.lineno and line_old_comments:
579 ${inline_comments_container(line_old_comments, inline_comments)}
579 ${inline_comments_container(line_old_comments, inline_comments)}
580 %endif
580 %endif
581
581
582 </td>
582 </td>
583 <td class="cb-data ${action_class(line.modified.action)}"
583 <td class="cb-data ${action_class(line.modified.action)}"
584 data-line-no="${line.modified.lineno}"
584 data-line-no="${line.modified.lineno}"
585 >
585 >
586 <div>
586 <div>
587
587
588 %if line.modified.get_comment_args:
588 %if line.modified.get_comment_args:
589 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
589 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
590 %else:
590 %else:
591 <% line_new_comments = None%>
591 <% line_new_comments = None%>
592 %endif
592 %endif
593 %if line_new_comments:
593 %if line_new_comments:
594 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
594 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
595 % if has_outdated:
595 % if has_outdated:
596 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
596 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
597 % else:
597 % else:
598 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
598 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
599 % endif
599 % endif
600 %endif
600 %endif
601 </div>
601 </div>
602 </td>
602 </td>
603 <td class="cb-lineno ${action_class(line.modified.action)}"
603 <td class="cb-lineno ${action_class(line.modified.action)}"
604 data-line-no="${line.modified.lineno}"
604 data-line-no="${line.modified.lineno}"
605 %if new_line_anchor:
605 %if new_line_anchor:
606 id="${new_line_anchor}"
606 id="${new_line_anchor}"
607 %endif
607 %endif
608 >
608 >
609 %if line.modified.lineno:
609 %if line.modified.lineno:
610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
611 %endif
611 %endif
612 </td>
612 </td>
613 <td class="cb-content ${action_class(line.modified.action)}"
613 <td class="cb-content ${action_class(line.modified.action)}"
614 data-line-no="n${line.modified.lineno}"
614 data-line-no="n${line.modified.lineno}"
615 >
615 >
616 %if use_comments and line.modified.lineno:
616 %if use_comments and line.modified.lineno:
617 ${render_add_comment_button()}
617 ${render_add_comment_button()}
618 %endif
618 %endif
619 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
619 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
620 %if use_comments and line.modified.lineno and line_new_comments:
620 %if use_comments and line.modified.lineno and line_new_comments:
621 ${inline_comments_container(line_new_comments, inline_comments)}
621 ${inline_comments_container(line_new_comments, inline_comments)}
622 %endif
622 %endif
623 </td>
623 </td>
624 </tr>
624 </tr>
625 %endfor
625 %endfor
626 </%def>
626 </%def>
627
627
628
628
629 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
629 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
630 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
630 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
631
631
632 <%
632 <%
633 old_line_anchor, new_line_anchor = None, None
633 old_line_anchor, new_line_anchor = None, None
634 if old_line_no:
634 if old_line_no:
635 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
635 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
636 if new_line_no:
636 if new_line_no:
637 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
637 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
638 %>
638 %>
639 <tr class="cb-line">
639 <tr class="cb-line">
640 <td class="cb-data ${action_class(action)}">
640 <td class="cb-data ${action_class(action)}">
641 <div>
641 <div>
642
642
643 %if comments_args:
643 %if comments_args:
644 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
644 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
645 %else:
645 %else:
646 <% comments = None %>
646 <% comments = None %>
647 %endif
647 %endif
648
648
649 % if comments:
649 % if comments:
650 <% has_outdated = any([x.outdated for x in comments]) %>
650 <% has_outdated = any([x.outdated for x in comments]) %>
651 % if has_outdated:
651 % if has_outdated:
652 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
652 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
653 % else:
653 % else:
654 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
654 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
655 % endif
655 % endif
656 % endif
656 % endif
657 </div>
657 </div>
658 </td>
658 </td>
659 <td class="cb-lineno ${action_class(action)}"
659 <td class="cb-lineno ${action_class(action)}"
660 data-line-no="${old_line_no}"
660 data-line-no="${old_line_no}"
661 %if old_line_anchor:
661 %if old_line_anchor:
662 id="${old_line_anchor}"
662 id="${old_line_anchor}"
663 %endif
663 %endif
664 >
664 >
665 %if old_line_anchor:
665 %if old_line_anchor:
666 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
666 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
667 %endif
667 %endif
668 </td>
668 </td>
669 <td class="cb-lineno ${action_class(action)}"
669 <td class="cb-lineno ${action_class(action)}"
670 data-line-no="${new_line_no}"
670 data-line-no="${new_line_no}"
671 %if new_line_anchor:
671 %if new_line_anchor:
672 id="${new_line_anchor}"
672 id="${new_line_anchor}"
673 %endif
673 %endif
674 >
674 >
675 %if new_line_anchor:
675 %if new_line_anchor:
676 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
676 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
677 %endif
677 %endif
678 </td>
678 </td>
679 <td class="cb-content ${action_class(action)}"
679 <td class="cb-content ${action_class(action)}"
680 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
680 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
681 >
681 >
682 %if use_comments:
682 %if use_comments:
683 ${render_add_comment_button()}
683 ${render_add_comment_button()}
684 %endif
684 %endif
685 <span class="cb-code">${action} ${content or '' | n}</span>
685 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
686 %if use_comments and comments:
686 %if use_comments and comments:
687 ${inline_comments_container(comments, inline_comments)}
687 ${inline_comments_container(comments, inline_comments)}
688 %endif
688 %endif
689 </td>
689 </td>
690 </tr>
690 </tr>
691 %endfor
691 %endfor
692 </%def>
692 </%def>
693
693
694
694
695 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
695 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
696 % if diff_mode == 'unified':
696 % if diff_mode == 'unified':
697 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
697 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
698 % elif diff_mode == 'sideside':
698 % elif diff_mode == 'sideside':
699 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
699 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
700 % else:
700 % else:
701 <tr class="cb-line">
701 <tr class="cb-line">
702 <td>unknown diff mode</td>
702 <td>unknown diff mode</td>
703 </tr>
703 </tr>
704 % endif
704 % endif
705 </%def>file changes
705 </%def>file changes
706
706
707
707
708 <%def name="render_add_comment_button()">
708 <%def name="render_add_comment_button()">
709 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
709 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
710 <span><i class="icon-comment"></i></span>
710 <span><i class="icon-comment"></i></span>
711 </button>
711 </button>
712 </%def>
712 </%def>
713
713
714 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
714 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
715
715
716 <div id="diff-file-sticky" class="diffset-menu clearinner">
716 <div id="diff-file-sticky" class="diffset-menu clearinner">
717 ## auto adjustable
717 ## auto adjustable
718 <div class="sidebar__inner">
718 <div class="sidebar__inner">
719 <div class="sidebar__bar">
719 <div class="sidebar__bar">
720 <div class="pull-right">
720 <div class="pull-right">
721 <div class="btn-group">
721 <div class="btn-group">
722
722
723 ## DIFF OPTIONS via Select2
723 ## DIFF OPTIONS via Select2
724 <div class="pull-left">
724 <div class="pull-left">
725 ${h.hidden('diff_menu')}
725 ${h.hidden('diff_menu')}
726 </div>
726 </div>
727
727
728 <a
728 <a
729 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
729 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
730 title="${h.tooltip(_('View side by side'))}"
730 title="${h.tooltip(_('View side by side'))}"
731 href="${h.current_route_path(request, diffmode='sideside')}">
731 href="${h.current_route_path(request, diffmode='sideside')}">
732 <span>${_('Side by Side')}</span>
732 <span>${_('Side by Side')}</span>
733 </a>
733 </a>
734
734
735 <a
735 <a
736 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
736 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
737 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
737 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
738 <span>${_('Unified')}</span>
738 <span>${_('Unified')}</span>
739 </a>
739 </a>
740
740
741 % if range_diff_on is True:
741 % if range_diff_on is True:
742 <a
742 <a
743 title="${_('Turn off: Show the diff as commit range')}"
743 title="${_('Turn off: Show the diff as commit range')}"
744 class="btn btn-primary"
744 class="btn btn-primary"
745 href="${h.current_route_path(request, **{"range-diff":"0"})}">
745 href="${h.current_route_path(request, **{"range-diff":"0"})}">
746 <span>${_('Range Diff')}</span>
746 <span>${_('Range Diff')}</span>
747 </a>
747 </a>
748 % elif range_diff_on is False:
748 % elif range_diff_on is False:
749 <a
749 <a
750 title="${_('Show the diff as commit range')}"
750 title="${_('Show the diff as commit range')}"
751 class="btn"
751 class="btn"
752 href="${h.current_route_path(request, **{"range-diff":"1"})}">
752 href="${h.current_route_path(request, **{"range-diff":"1"})}">
753 <span>${_('Range Diff')}</span>
753 <span>${_('Range Diff')}</span>
754 </a>
754 </a>
755 % endif
755 % endif
756 </div>
756 </div>
757 </div>
757 </div>
758 <div class="pull-left">
758 <div class="pull-left">
759 <div class="btn-group">
759 <div class="btn-group">
760 <div class="pull-left">
760 <div class="pull-left">
761 ${h.hidden('file_filter')}
761 ${h.hidden('file_filter')}
762 </div>
762 </div>
763 <a
763 <a
764 class="btn"
764 class="btn"
765 href="#"
765 href="#"
766 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
766 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
767 <a
767 <a
768 class="btn"
768 class="btn"
769 href="#"
769 href="#"
770 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
770 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
771 </div>
771 </div>
772 </div>
772 </div>
773 </div>
773 </div>
774 <div class="fpath-placeholder">
774 <div class="fpath-placeholder">
775 <i class="icon-file-text"></i>
775 <i class="icon-file-text"></i>
776 <strong class="fpath-placeholder-text">
776 <strong class="fpath-placeholder-text">
777 Context file:
777 Context file:
778 </strong>
778 </strong>
779 </div>
779 </div>
780 <div class="sidebar_inner_shadow"></div>
780 <div class="sidebar_inner_shadow"></div>
781 </div>
781 </div>
782 </div>
782 </div>
783
783
784 % if diffset:
784 % if diffset:
785
785
786 %if diffset.limited_diff:
786 %if diffset.limited_diff:
787 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
787 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
788 %else:
788 %else:
789 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
789 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
790 %endif
790 %endif
791 ## case on range-diff placeholder needs to be updated
791 ## case on range-diff placeholder needs to be updated
792 % if range_diff_on is True:
792 % if range_diff_on is True:
793 <% file_placeholder = _('Disabled on range diff') %>
793 <% file_placeholder = _('Disabled on range diff') %>
794 % endif
794 % endif
795
795
796 <script>
796 <script>
797
797
798 var feedFilesOptions = function (query, initialData) {
798 var feedFilesOptions = function (query, initialData) {
799 var data = {results: []};
799 var data = {results: []};
800 var isQuery = typeof query.term !== 'undefined';
800 var isQuery = typeof query.term !== 'undefined';
801
801
802 var section = _gettext('Changed files');
802 var section = _gettext('Changed files');
803 var filteredData = [];
803 var filteredData = [];
804
804
805 //filter results
805 //filter results
806 $.each(initialData.results, function (idx, value) {
806 $.each(initialData.results, function (idx, value) {
807
807
808 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
808 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
809 filteredData.push({
809 filteredData.push({
810 'id': this.id,
810 'id': this.id,
811 'text': this.text,
811 'text': this.text,
812 "ops": this.ops,
812 "ops": this.ops,
813 })
813 })
814 }
814 }
815
815
816 });
816 });
817
817
818 data.results = filteredData;
818 data.results = filteredData;
819
819
820 query.callback(data);
820 query.callback(data);
821 };
821 };
822
822
823 var formatFileResult = function(result, container, query, escapeMarkup) {
823 var formatFileResult = function(result, container, query, escapeMarkup) {
824 return function(data, escapeMarkup) {
824 return function(data, escapeMarkup) {
825 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
825 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
826 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
826 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
827 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
827 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
828 '<span class="pill" op="added">{0}</span>' +
828 '<span class="pill" op="added">{0}</span>' +
829 '<span class="pill" op="deleted">{1}</span>' +
829 '<span class="pill" op="deleted">{1}</span>' +
830 '</span>'
830 '</span>'
831 ;
831 ;
832 var added = data['ops']['added'];
832 var added = data['ops']['added'];
833 if (added === 0) {
833 if (added === 0) {
834 // don't show +0
834 // don't show +0
835 added = 0;
835 added = 0;
836 } else {
836 } else {
837 added = '+' + added;
837 added = '+' + added;
838 }
838 }
839
839
840 var deleted = -1*data['ops']['deleted'];
840 var deleted = -1*data['ops']['deleted'];
841
841
842 tmpl += pill.format(added, deleted);
842 tmpl += pill.format(added, deleted);
843 return container.format(tmpl);
843 return container.format(tmpl);
844
844
845 }(result, escapeMarkup);
845 }(result, escapeMarkup);
846 };
846 };
847 var preloadData = {
847 var preloadData = {
848 results: [
848 results: [
849 % for filediff in diffset.files:
849 % for filediff in diffset.files:
850 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
850 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
851 text:"${filediff.patch['filename']}",
851 text:"${filediff.patch['filename']}",
852 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
852 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
853 % endfor
853 % endfor
854 ]
854 ]
855 };
855 };
856
856
857 $(document).ready(function () {
857 $(document).ready(function () {
858
858
859 var fileFilter = $("#file_filter").select2({
859 var fileFilter = $("#file_filter").select2({
860 'dropdownAutoWidth': true,
860 'dropdownAutoWidth': true,
861 'width': 'auto',
861 'width': 'auto',
862 'placeholder': "${file_placeholder}",
862 'placeholder': "${file_placeholder}",
863 containerCssClass: "drop-menu",
863 containerCssClass: "drop-menu",
864 dropdownCssClass: "drop-menu-dropdown",
864 dropdownCssClass: "drop-menu-dropdown",
865 data: preloadData,
865 data: preloadData,
866 query: function(query) {
866 query: function(query) {
867 feedFilesOptions(query, preloadData);
867 feedFilesOptions(query, preloadData);
868 },
868 },
869 formatResult: formatFileResult
869 formatResult: formatFileResult
870 });
870 });
871 % if range_diff_on is True:
871 % if range_diff_on is True:
872 fileFilter.select2("enable", false);
872 fileFilter.select2("enable", false);
873
873
874 % endif
874 % endif
875
875
876 $("#file_filter").on('click', function (e) {
876 $("#file_filter").on('click', function (e) {
877 e.preventDefault();
877 e.preventDefault();
878 var selected = $('#file_filter').select2('data');
878 var selected = $('#file_filter').select2('data');
879 var idSelector = "#"+selected.id;
879 var idSelector = "#"+selected.id;
880 window.location.hash = idSelector;
880 window.location.hash = idSelector;
881 // expand the container if we quick-select the field
881 // expand the container if we quick-select the field
882 $(idSelector).next().prop('checked', false);
882 $(idSelector).next().prop('checked', false);
883 updateSticky()
883 updateSticky()
884 });
884 });
885
885
886 var contextPrefix = _gettext('Context file: ');
886 var contextPrefix = _gettext('Context file: ');
887 ## sticky sidebar
887 ## sticky sidebar
888 var sidebarElement = document.getElementById('diff-file-sticky');
888 var sidebarElement = document.getElementById('diff-file-sticky');
889 sidebar = new StickySidebar(sidebarElement, {
889 sidebar = new StickySidebar(sidebarElement, {
890 topSpacing: 0,
890 topSpacing: 0,
891 bottomSpacing: 0,
891 bottomSpacing: 0,
892 innerWrapperSelector: '.sidebar__inner'
892 innerWrapperSelector: '.sidebar__inner'
893 });
893 });
894 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
894 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
895 // reset our file so it's not holding new value
895 // reset our file so it's not holding new value
896 $('.fpath-placeholder-text').html(contextPrefix)
896 $('.fpath-placeholder-text').html(contextPrefix)
897 });
897 });
898
898
899 updateSticky = function () {
899 updateSticky = function () {
900 sidebar.updateSticky();
900 sidebar.updateSticky();
901 Waypoint.refreshAll();
901 Waypoint.refreshAll();
902 };
902 };
903
903
904 var animateText = $.debounce(100, function(fPath, anchorId) {
904 var animateText = $.debounce(100, function(fPath, anchorId) {
905 // animate setting the text
905 // animate setting the text
906 var callback = function () {
906 var callback = function () {
907 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
907 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
908 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
908 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
909 };
909 };
910 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
910 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
911 });
911 });
912
912
913 ## dynamic file waypoints
913 ## dynamic file waypoints
914 var setFPathInfo = function(fPath, anchorId){
914 var setFPathInfo = function(fPath, anchorId){
915 animateText(fPath, anchorId)
915 animateText(fPath, anchorId)
916 };
916 };
917
917
918 var codeBlock = $('.filediff');
918 var codeBlock = $('.filediff');
919 // forward waypoint
919 // forward waypoint
920 codeBlock.waypoint(
920 codeBlock.waypoint(
921 function(direction) {
921 function(direction) {
922 if (direction === "down"){
922 if (direction === "down"){
923 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
923 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
924 }
924 }
925 }, {
925 }, {
926 offset: 70,
926 offset: 70,
927 context: '.fpath-placeholder'
927 context: '.fpath-placeholder'
928 }
928 }
929 );
929 );
930
930
931 // backward waypoint
931 // backward waypoint
932 codeBlock.waypoint(
932 codeBlock.waypoint(
933 function(direction) {
933 function(direction) {
934 if (direction === "up"){
934 if (direction === "up"){
935 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
935 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
936 }
936 }
937 }, {
937 }, {
938 offset: function () {
938 offset: function () {
939 return -this.element.clientHeight + 90
939 return -this.element.clientHeight + 90
940 },
940 },
941 context: '.fpath-placeholder'
941 context: '.fpath-placeholder'
942 }
942 }
943 );
943 );
944
944
945 var preloadData = {
945 var preloadData = {
946 results: [
946 results: [
947 ## Wide diff mode
947 ## Wide diff mode
948 {
948 {
949 id: 1,
949 id: 1,
950 text: _gettext('Toggle Wide Mode Diff'),
950 text: _gettext('Toggle Wide Mode diff'),
951 action: function () {
951 action: function () {
952 updateSticky();
952 updateSticky();
953 Rhodecode.comments.toggleWideMode(this);
953 Rhodecode.comments.toggleWideMode(this);
954 return null;
954 return null;
955 },
955 },
956 url: null,
956 url: null,
957 },
957 },
958
958
959 ## Whitespace change
959 ## Whitespace change
960 % if request.GET.get('ignorews', '') == '1':
960 % if request.GET.get('ignorews', '') == '1':
961 {
961 {
962 id: 2,
962 id: 2,
963 text: _gettext('Show whitespace changes'),
963 text: _gettext('Show whitespace changes'),
964 action: function () {},
964 action: function () {},
965 url: "${h.current_route_path(request, ignorews=0)|n}"
965 url: "${h.current_route_path(request, ignorews=0)|n}"
966 },
966 },
967 % else:
967 % else:
968 {
968 {
969 id: 2,
969 id: 2,
970 text: _gettext('Hide whitespace changes'),
970 text: _gettext('Hide whitespace changes'),
971 action: function () {},
971 action: function () {},
972 url: "${h.current_route_path(request, ignorews=1)|n}"
972 url: "${h.current_route_path(request, ignorews=1)|n}"
973 },
973 },
974 % endif
974 % endif
975
975
976 ## FULL CONTEXT
976 ## FULL CONTEXT
977 % if request.GET.get('fullcontext', '') == '1':
977 % if request.GET.get('fullcontext', '') == '1':
978 {
978 {
979 id: 3,
979 id: 3,
980 text: _gettext('Hide full context diff'),
980 text: _gettext('Hide full context diff'),
981 action: function () {},
981 action: function () {},
982 url: "${h.current_route_path(request, fullcontext=0)|n}"
982 url: "${h.current_route_path(request, fullcontext=0)|n}"
983 },
983 },
984 % else:
984 % else:
985 {
985 {
986 id: 3,
986 id: 3,
987 text: _gettext('Show full context diff'),
987 text: _gettext('Show full context diff'),
988 action: function () {},
988 action: function () {},
989 url: "${h.current_route_path(request, fullcontext=1)|n}"
989 url: "${h.current_route_path(request, fullcontext=1)|n}"
990 },
990 },
991 % endif
991 % endif
992
992
993 ]
993 ]
994 };
994 };
995
995
996 $("#diff_menu").select2({
996 $("#diff_menu").select2({
997 minimumResultsForSearch: -1,
997 minimumResultsForSearch: -1,
998 containerCssClass: "drop-menu",
998 containerCssClass: "drop-menu",
999 dropdownCssClass: "drop-menu-dropdown",
999 dropdownCssClass: "drop-menu-dropdown",
1000 dropdownAutoWidth: true,
1000 dropdownAutoWidth: true,
1001 data: preloadData,
1001 data: preloadData,
1002 placeholder: "${_('Diff Options')}",
1002 placeholder: "${_('Diff Options')}",
1003 });
1003 });
1004 $("#diff_menu").on('select2-selecting', function (e) {
1004 $("#diff_menu").on('select2-selecting', function (e) {
1005 e.choice.action();
1005 e.choice.action();
1006 if (e.choice.url !== null) {
1006 if (e.choice.url !== null) {
1007 window.location = e.choice.url
1007 window.location = e.choice.url
1008 }
1008 }
1009 });
1009 });
1010
1010
1011 });
1011 });
1012
1012
1013 </script>
1013 </script>
1014 % endif
1014 % endif
1015
1015
1016 </%def> No newline at end of file
1016 </%def>
General Comments 0
You need to be logged in to leave comments. Login now