##// END OF EJS Templates
compare: fixed case of cross repo compare before links not working.
marcink -
r3146:8bba6dde default
parent child Browse files
Show More
@@ -1,305 +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 not 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 not 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 not 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_new_diff_menus(self, response, right_menu=False,):
296 def _check_new_diff_menus(self, response, right_menu=False,):
297 # individual file diff menus
297 # individual file diff menus
298 for elem in ['Show file before', 'Show file after']:
298 for elem in ['Show file before', 'Show file after']:
299 response.mustcontain(elem)
299 response.mustcontain(elem)
300
300
301 # right pane diff menus
301 # right pane diff menus
302 if right_menu:
302 if right_menu:
303 for elem in ['Hide whitespace changes', 'Toggle Wide Mode diff',
303 for elem in ['Hide whitespace changes', 'Toggle Wide Mode diff',
304 'Show full context diff']:
304 'Show full context diff']:
305 response.mustcontain(elem)
305 response.mustcontain(elem)
@@ -1,315 +1,316 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-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 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
30 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
34 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 from rhodecode.lib.vcs.exceptions import (
36 from rhodecode.lib.vcs.exceptions import (
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
37 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 NodeDoesNotExistError)
38 NodeDoesNotExistError)
39 from rhodecode.model.db import Repository, ChangesetStatus
39 from rhodecode.model.db import Repository, ChangesetStatus
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class RepoCompareView(RepoAppView):
44 class RepoCompareView(RepoAppView):
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context(include_app_defaults=True)
46 c = self._get_local_tmpl_context(include_app_defaults=True)
47 c.rhodecode_repo = self.rhodecode_vcs_repo
47 c.rhodecode_repo = self.rhodecode_vcs_repo
48 return c
48 return c
49
49
50 def _get_commit_or_redirect(
50 def _get_commit_or_redirect(
51 self, ref, ref_type, repo, redirect_after=True, partial=False):
51 self, ref, ref_type, repo, redirect_after=True, partial=False):
52 """
52 """
53 This is a safe way to get a commit. If an error occurs it
53 This is a safe way to get a commit. If an error occurs it
54 redirects to a commit with a proper message. If partial is set
54 redirects to a commit with a proper message. If partial is set
55 then it does not do redirect raise and throws an exception instead.
55 then it does not do redirect raise and throws an exception instead.
56 """
56 """
57 _ = self.request.translate
57 _ = self.request.translate
58 try:
58 try:
59 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
59 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
60 except EmptyRepositoryError:
60 except EmptyRepositoryError:
61 if not redirect_after:
61 if not redirect_after:
62 return repo.scm_instance().EMPTY_COMMIT
62 return repo.scm_instance().EMPTY_COMMIT
63 h.flash(h.literal(_('There are no commits yet')),
63 h.flash(h.literal(_('There are no commits yet')),
64 category='warning')
64 category='warning')
65 if not partial:
65 if not partial:
66 raise HTTPFound(
66 raise HTTPFound(
67 h.route_path('repo_summary', repo_name=repo.repo_name))
67 h.route_path('repo_summary', repo_name=repo.repo_name))
68 raise HTTPBadRequest()
68 raise HTTPBadRequest()
69
69
70 except RepositoryError as e:
70 except RepositoryError as e:
71 log.exception(safe_str(e))
71 log.exception(safe_str(e))
72 h.flash(safe_str(h.escape(e)), category='warning')
72 h.flash(safe_str(h.escape(e)), category='warning')
73 if not partial:
73 if not partial:
74 raise HTTPFound(
74 raise HTTPFound(
75 h.route_path('repo_summary', repo_name=repo.repo_name))
75 h.route_path('repo_summary', repo_name=repo.repo_name))
76 raise HTTPBadRequest()
76 raise HTTPBadRequest()
77
77
78 @LoginRequired()
78 @LoginRequired()
79 @HasRepoPermissionAnyDecorator(
79 @HasRepoPermissionAnyDecorator(
80 'repository.read', 'repository.write', 'repository.admin')
80 'repository.read', 'repository.write', 'repository.admin')
81 @view_config(
81 @view_config(
82 route_name='repo_compare_select', request_method='GET',
82 route_name='repo_compare_select', request_method='GET',
83 renderer='rhodecode:templates/compare/compare_diff.mako')
83 renderer='rhodecode:templates/compare/compare_diff.mako')
84 def compare_select(self):
84 def compare_select(self):
85 _ = self.request.translate
85 _ = self.request.translate
86 c = self.load_default_context()
86 c = self.load_default_context()
87
87
88 source_repo = self.db_repo_name
88 source_repo = self.db_repo_name
89 target_repo = self.request.GET.get('target_repo', source_repo)
89 target_repo = self.request.GET.get('target_repo', source_repo)
90 c.source_repo = Repository.get_by_repo_name(source_repo)
90 c.source_repo = Repository.get_by_repo_name(source_repo)
91 c.target_repo = Repository.get_by_repo_name(target_repo)
91 c.target_repo = Repository.get_by_repo_name(target_repo)
92
92
93 if c.source_repo is None or c.target_repo is None:
93 if c.source_repo is None or c.target_repo is None:
94 raise HTTPNotFound()
94 raise HTTPNotFound()
95
95
96 c.compare_home = True
96 c.compare_home = True
97 c.commit_ranges = []
97 c.commit_ranges = []
98 c.collapse_all_commits = False
98 c.collapse_all_commits = False
99 c.diffset = None
99 c.diffset = None
100 c.limited_diff = False
100 c.limited_diff = False
101 c.source_ref = c.target_ref = _('Select commit')
101 c.source_ref = c.target_ref = _('Select commit')
102 c.source_ref_type = ""
102 c.source_ref_type = ""
103 c.target_ref_type = ""
103 c.target_ref_type = ""
104 c.commit_statuses = ChangesetStatus.STATUSES
104 c.commit_statuses = ChangesetStatus.STATUSES
105 c.preview_mode = False
105 c.preview_mode = False
106 c.file_path = None
106 c.file_path = None
107
107
108 return self._get_template_context(c)
108 return self._get_template_context(c)
109
109
110 @LoginRequired()
110 @LoginRequired()
111 @HasRepoPermissionAnyDecorator(
111 @HasRepoPermissionAnyDecorator(
112 'repository.read', 'repository.write', 'repository.admin')
112 'repository.read', 'repository.write', 'repository.admin')
113 @view_config(
113 @view_config(
114 route_name='repo_compare', request_method='GET',
114 route_name='repo_compare', request_method='GET',
115 renderer=None)
115 renderer=None)
116 def compare(self):
116 def compare(self):
117 _ = self.request.translate
117 _ = self.request.translate
118 c = self.load_default_context()
118 c = self.load_default_context()
119
119
120 source_ref_type = self.request.matchdict['source_ref_type']
120 source_ref_type = self.request.matchdict['source_ref_type']
121 source_ref = self.request.matchdict['source_ref']
121 source_ref = self.request.matchdict['source_ref']
122 target_ref_type = self.request.matchdict['target_ref_type']
122 target_ref_type = self.request.matchdict['target_ref_type']
123 target_ref = self.request.matchdict['target_ref']
123 target_ref = self.request.matchdict['target_ref']
124
124
125 # source_ref will be evaluated in source_repo
125 # source_ref will be evaluated in source_repo
126 source_repo_name = self.db_repo_name
126 source_repo_name = self.db_repo_name
127 source_path, source_id = parse_path_ref(source_ref)
127 source_path, source_id = parse_path_ref(source_ref)
128
128
129 # target_ref will be evaluated in target_repo
129 # target_ref will be evaluated in target_repo
130 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
130 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
131 target_path, target_id = parse_path_ref(
131 target_path, target_id = parse_path_ref(
132 target_ref, default_path=self.request.GET.get('f_path', ''))
132 target_ref, default_path=self.request.GET.get('f_path', ''))
133
133
134 # if merge is True
134 # if merge is True
135 # Show what changes since the shared ancestor commit of target/source
135 # Show what changes since the shared ancestor commit of target/source
136 # the source would get if it was merged with target. Only commits
136 # the source would get if it was merged with target. Only commits
137 # which are in target but not in source will be shown.
137 # which are in target but not in source will be shown.
138 merge = str2bool(self.request.GET.get('merge'))
138 merge = str2bool(self.request.GET.get('merge'))
139 # if merge is False
139 # if merge is False
140 # Show a raw diff of source/target refs even if no ancestor exists
140 # Show a raw diff of source/target refs even if no ancestor exists
141
141
142 # c.fulldiff disables cut_off_limit
142 # c.fulldiff disables cut_off_limit
143 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
143 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
144
144
145 # fetch global flags of ignore ws or context lines
145 # fetch global flags of ignore ws or context lines
146 diff_context = diffs.get_diff_context(self.request)
146 diff_context = diffs.get_diff_context(self.request)
147 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
147 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
148
148
149 c.file_path = target_path
149 c.file_path = target_path
150 c.commit_statuses = ChangesetStatus.STATUSES
150 c.commit_statuses = ChangesetStatus.STATUSES
151
151
152 # if partial, returns just compare_commits.html (commits log)
152 # if partial, returns just compare_commits.html (commits log)
153 partial = self.request.is_xhr
153 partial = self.request.is_xhr
154
154
155 # swap url for compare_diff page
155 # swap url for compare_diff page
156 c.swap_url = h.route_path(
156 c.swap_url = h.route_path(
157 'repo_compare',
157 'repo_compare',
158 repo_name=target_repo_name,
158 repo_name=target_repo_name,
159 source_ref_type=target_ref_type,
159 source_ref_type=target_ref_type,
160 source_ref=target_ref,
160 source_ref=target_ref,
161 target_repo=source_repo_name,
161 target_repo=source_repo_name,
162 target_ref_type=source_ref_type,
162 target_ref_type=source_ref_type,
163 target_ref=source_ref,
163 target_ref=source_ref,
164 _query=dict(merge=merge and '1' or '', f_path=target_path))
164 _query=dict(merge=merge and '1' or '', f_path=target_path))
165
165
166 source_repo = Repository.get_by_repo_name(source_repo_name)
166 source_repo = Repository.get_by_repo_name(source_repo_name)
167 target_repo = Repository.get_by_repo_name(target_repo_name)
167 target_repo = Repository.get_by_repo_name(target_repo_name)
168
168
169 if source_repo is None:
169 if source_repo is None:
170 log.error('Could not find the source repo: {}'
170 log.error('Could not find the source repo: {}'
171 .format(source_repo_name))
171 .format(source_repo_name))
172 h.flash(_('Could not find the source repo: `{}`')
172 h.flash(_('Could not find the source repo: `{}`')
173 .format(h.escape(source_repo_name)), category='error')
173 .format(h.escape(source_repo_name)), category='error')
174 raise HTTPFound(
174 raise HTTPFound(
175 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
175 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
176
176
177 if target_repo is None:
177 if target_repo is None:
178 log.error('Could not find the target repo: {}'
178 log.error('Could not find the target repo: {}'
179 .format(source_repo_name))
179 .format(source_repo_name))
180 h.flash(_('Could not find the target repo: `{}`')
180 h.flash(_('Could not find the target repo: `{}`')
181 .format(h.escape(target_repo_name)), category='error')
181 .format(h.escape(target_repo_name)), category='error')
182 raise HTTPFound(
182 raise HTTPFound(
183 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
183 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
184
184
185 source_scm = source_repo.scm_instance()
185 source_scm = source_repo.scm_instance()
186 target_scm = target_repo.scm_instance()
186 target_scm = target_repo.scm_instance()
187
187
188 source_alias = source_scm.alias
188 source_alias = source_scm.alias
189 target_alias = target_scm.alias
189 target_alias = target_scm.alias
190 if source_alias != target_alias:
190 if source_alias != target_alias:
191 msg = _('The comparison of two different kinds of remote repos '
191 msg = _('The comparison of two different kinds of remote repos '
192 'is not available')
192 'is not available')
193 log.error(msg)
193 log.error(msg)
194 h.flash(msg, category='error')
194 h.flash(msg, category='error')
195 raise HTTPFound(
195 raise HTTPFound(
196 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
196 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
197
197
198 source_commit = self._get_commit_or_redirect(
198 source_commit = self._get_commit_or_redirect(
199 ref=source_id, ref_type=source_ref_type, repo=source_repo,
199 ref=source_id, ref_type=source_ref_type, repo=source_repo,
200 partial=partial)
200 partial=partial)
201 target_commit = self._get_commit_or_redirect(
201 target_commit = self._get_commit_or_redirect(
202 ref=target_id, ref_type=target_ref_type, repo=target_repo,
202 ref=target_id, ref_type=target_ref_type, repo=target_repo,
203 partial=partial)
203 partial=partial)
204
204
205 c.compare_home = False
205 c.compare_home = False
206 c.source_repo = source_repo
206 c.source_repo = source_repo
207 c.target_repo = target_repo
207 c.target_repo = target_repo
208 c.source_ref = source_ref
208 c.source_ref = source_ref
209 c.target_ref = target_ref
209 c.target_ref = target_ref
210 c.source_ref_type = source_ref_type
210 c.source_ref_type = source_ref_type
211 c.target_ref_type = target_ref_type
211 c.target_ref_type = target_ref_type
212
212
213 pre_load = ["author", "branch", "date", "message"]
213 pre_load = ["author", "branch", "date", "message"]
214 c.ancestor = None
214 c.ancestor = None
215
215
216 if c.file_path:
216 if c.file_path:
217 if source_commit == target_commit:
217 if source_commit == target_commit:
218 c.commit_ranges = []
218 c.commit_ranges = []
219 else:
219 else:
220 c.commit_ranges = [target_commit]
220 c.commit_ranges = [target_commit]
221 else:
221 else:
222 try:
222 try:
223 c.commit_ranges = source_scm.compare(
223 c.commit_ranges = source_scm.compare(
224 source_commit.raw_id, target_commit.raw_id,
224 source_commit.raw_id, target_commit.raw_id,
225 target_scm, merge, pre_load=pre_load)
225 target_scm, merge, pre_load=pre_load)
226 if merge:
226 if merge:
227 c.ancestor = source_scm.get_common_ancestor(
227 c.ancestor = source_scm.get_common_ancestor(
228 source_commit.raw_id, target_commit.raw_id, target_scm)
228 source_commit.raw_id, target_commit.raw_id, target_scm)
229 except RepositoryRequirementError:
229 except RepositoryRequirementError:
230 msg = _('Could not compare repos with different '
230 msg = _('Could not compare repos with different '
231 'large file settings')
231 'large file settings')
232 log.error(msg)
232 log.error(msg)
233 if partial:
233 if partial:
234 return Response(msg)
234 return Response(msg)
235 h.flash(msg, category='error')
235 h.flash(msg, category='error')
236 raise HTTPFound(
236 raise HTTPFound(
237 h.route_path('repo_compare_select',
237 h.route_path('repo_compare_select',
238 repo_name=self.db_repo_name))
238 repo_name=self.db_repo_name))
239
239
240 c.statuses = self.db_repo.statuses(
240 c.statuses = self.db_repo.statuses(
241 [x.raw_id for x in c.commit_ranges])
241 [x.raw_id for x in c.commit_ranges])
242
242
243 # auto collapse if we have more than limit
243 # auto collapse if we have more than limit
244 collapse_limit = diffs.DiffProcessor._collapse_commits_over
244 collapse_limit = diffs.DiffProcessor._collapse_commits_over
245 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
245 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
246
246
247 if partial: # for PR ajax commits loader
247 if partial: # for PR ajax commits loader
248 if not c.ancestor:
248 if not c.ancestor:
249 return Response('') # cannot merge if there is no ancestor
249 return Response('') # cannot merge if there is no ancestor
250
250
251 html = render(
251 html = render(
252 'rhodecode:templates/compare/compare_commits.mako',
252 'rhodecode:templates/compare/compare_commits.mako',
253 self._get_template_context(c), self.request)
253 self._get_template_context(c), self.request)
254 return Response(html)
254 return Response(html)
255
255
256 if c.ancestor:
256 if c.ancestor:
257 # case we want a simple diff without incoming commits,
257 # case we want a simple diff without incoming commits,
258 # previewing what will be merged.
258 # previewing what will be merged.
259 # Make the diff on target repo (which is known to have target_ref)
259 # Make the diff on target repo (which is known to have target_ref)
260 log.debug('Using ancestor %s as source_ref instead of %s',
260 log.debug('Using ancestor %s as source_ref instead of %s',
261 c.ancestor, source_ref)
261 c.ancestor, source_ref)
262 source_repo = target_repo
262 source_repo = target_repo
263 source_commit = target_repo.get_commit(commit_id=c.ancestor)
263 source_commit = target_repo.get_commit(commit_id=c.ancestor)
264
264
265 # diff_limit will cut off the whole diff if the limit is applied
265 # diff_limit will cut off the whole diff if the limit is applied
266 # otherwise it will just hide the big files from the front-end
266 # otherwise it will just hide the big files from the front-end
267 diff_limit = c.visual.cut_off_limit_diff
267 diff_limit = c.visual.cut_off_limit_diff
268 file_limit = c.visual.cut_off_limit_file
268 file_limit = c.visual.cut_off_limit_file
269
269
270 log.debug('calculating diff between '
270 log.debug('calculating diff between '
271 'source_ref:%s and target_ref:%s for repo `%s`',
271 'source_ref:%s and target_ref:%s for repo `%s`',
272 source_commit, target_commit,
272 source_commit, target_commit,
273 safe_unicode(source_repo.scm_instance().path))
273 safe_unicode(source_repo.scm_instance().path))
274
274
275 if source_commit.repository != target_commit.repository:
275 if source_commit.repository != target_commit.repository:
276 msg = _(
276 msg = _(
277 "Repositories unrelated. "
277 "Repositories unrelated. "
278 "Cannot compare commit %(commit1)s from repository %(repo1)s "
278 "Cannot compare commit %(commit1)s from repository %(repo1)s "
279 "with commit %(commit2)s from repository %(repo2)s.") % {
279 "with commit %(commit2)s from repository %(repo2)s.") % {
280 'commit1': h.show_id(source_commit),
280 'commit1': h.show_id(source_commit),
281 'repo1': source_repo.repo_name,
281 'repo1': source_repo.repo_name,
282 'commit2': h.show_id(target_commit),
282 'commit2': h.show_id(target_commit),
283 'repo2': target_repo.repo_name,
283 'repo2': target_repo.repo_name,
284 }
284 }
285 h.flash(msg, category='error')
285 h.flash(msg, category='error')
286 raise HTTPFound(
286 raise HTTPFound(
287 h.route_path('repo_compare_select',
287 h.route_path('repo_compare_select',
288 repo_name=self.db_repo_name))
288 repo_name=self.db_repo_name))
289
289
290 txt_diff = source_repo.scm_instance().get_diff(
290 txt_diff = source_repo.scm_instance().get_diff(
291 commit1=source_commit, commit2=target_commit,
291 commit1=source_commit, commit2=target_commit,
292 path=target_path, path1=source_path,
292 path=target_path, path1=source_path,
293 ignore_whitespace=hide_whitespace_changes, context=diff_context)
293 ignore_whitespace=hide_whitespace_changes, context=diff_context)
294
294
295 diff_processor = diffs.DiffProcessor(
295 diff_processor = diffs.DiffProcessor(
296 txt_diff, format='newdiff', diff_limit=diff_limit,
296 txt_diff, format='newdiff', diff_limit=diff_limit,
297 file_limit=file_limit, show_full_diff=c.fulldiff)
297 file_limit=file_limit, show_full_diff=c.fulldiff)
298 _parsed = diff_processor.prepare()
298 _parsed = diff_processor.prepare()
299
299
300 diffset = codeblocks.DiffSet(
300 diffset = codeblocks.DiffSet(
301 repo_name=source_repo.repo_name,
301 repo_name=source_repo.repo_name,
302 source_node_getter=codeblocks.diffset_node_getter(source_commit),
302 source_node_getter=codeblocks.diffset_node_getter(source_commit),
303 target_repo_name=self.db_repo_name,
303 target_node_getter=codeblocks.diffset_node_getter(target_commit),
304 target_node_getter=codeblocks.diffset_node_getter(target_commit),
304 )
305 )
305 c.diffset = self.path_filter.render_patchset_filtered(
306 c.diffset = self.path_filter.render_patchset_filtered(
306 diffset, _parsed, source_ref, target_ref)
307 diffset, _parsed, source_ref, target_ref)
307
308
308 c.preview_mode = merge
309 c.preview_mode = merge
309 c.source_commit = source_commit
310 c.source_commit = source_commit
310 c.target_commit = target_commit
311 c.target_commit = target_commit
311
312
312 html = render(
313 html = render(
313 'rhodecode:templates/compare/compare_diff.mako',
314 'rhodecode:templates/compare/compare_diff.mako',
314 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
315 return Response(html) No newline at end of file
316 return Response(html)
@@ -1,771 +1,775 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 import logging
21 import logging
22 import difflib
22 import difflib
23 from itertools import groupby
23 from itertools import groupby
24
24
25 from pygments import lex
25 from pygments import lex
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
26 from pygments.formatters.html import _get_ttype_class as pygment_token_class
27 from pygments.lexers.special import TextLexer, Token
27 from pygments.lexers.special import TextLexer, Token
28 from pygments.lexers import get_lexer_by_name
28 from pygments.lexers import get_lexer_by_name
29
29
30 from rhodecode.lib.helpers import (
30 from rhodecode.lib.helpers import (
31 get_lexer_for_filenode, html_escape, get_custom_lexer)
31 get_lexer_for_filenode, html_escape, get_custom_lexer)
32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
32 from rhodecode.lib.utils2 import AttributeDict, StrictAttributeDict, safe_unicode
33 from rhodecode.lib.vcs.nodes import FileNode
33 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
34 from rhodecode.lib.vcs.exceptions import VCSError, NodeDoesNotExistError
35 from rhodecode.lib.diff_match_patch import diff_match_patch
35 from rhodecode.lib.diff_match_patch import diff_match_patch
36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
36 from rhodecode.lib.diffs import LimitedDiffContainer, DEL_FILENODE, BIN_FILENODE
37
37
38
38
39 plain_text_lexer = get_lexer_by_name(
39 plain_text_lexer = get_lexer_by_name(
40 'text', stripall=False, stripnl=False, ensurenl=False)
40 'text', stripall=False, stripnl=False, ensurenl=False)
41
41
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def filenode_as_lines_tokens(filenode, lexer=None):
46 def filenode_as_lines_tokens(filenode, lexer=None):
47 org_lexer = lexer
47 org_lexer = lexer
48 lexer = lexer or get_lexer_for_filenode(filenode)
48 lexer = lexer or get_lexer_for_filenode(filenode)
49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
49 log.debug('Generating file node pygment tokens for %s, %s, org_lexer:%s',
50 lexer, filenode, org_lexer)
50 lexer, filenode, org_lexer)
51 tokens = tokenize_string(filenode.content, lexer)
51 tokens = tokenize_string(filenode.content, lexer)
52 lines = split_token_stream(tokens)
52 lines = split_token_stream(tokens)
53 rv = list(lines)
53 rv = list(lines)
54 return rv
54 return rv
55
55
56
56
57 def tokenize_string(content, lexer):
57 def tokenize_string(content, lexer):
58 """
58 """
59 Use pygments to tokenize some content based on a lexer
59 Use pygments to tokenize some content based on a lexer
60 ensuring all original new lines and whitespace is preserved
60 ensuring all original new lines and whitespace is preserved
61 """
61 """
62
62
63 lexer.stripall = False
63 lexer.stripall = False
64 lexer.stripnl = False
64 lexer.stripnl = False
65 lexer.ensurenl = False
65 lexer.ensurenl = False
66
66
67 if isinstance(lexer, TextLexer):
67 if isinstance(lexer, TextLexer):
68 lexed = [(Token.Text, content)]
68 lexed = [(Token.Text, content)]
69 else:
69 else:
70 lexed = lex(content, lexer)
70 lexed = lex(content, lexer)
71
71
72 for token_type, token_text in lexed:
72 for token_type, token_text in lexed:
73 yield pygment_token_class(token_type), token_text
73 yield pygment_token_class(token_type), token_text
74
74
75
75
76 def split_token_stream(tokens):
76 def split_token_stream(tokens):
77 """
77 """
78 Take a list of (TokenType, text) tuples and split them by a string
78 Take a list of (TokenType, text) tuples and split them by a string
79
79
80 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
80 split_token_stream([(TEXT, 'some\ntext'), (TEXT, 'more\n')])
81 [(TEXT, 'some'), (TEXT, 'text'),
81 [(TEXT, 'some'), (TEXT, 'text'),
82 (TEXT, 'more'), (TEXT, 'text')]
82 (TEXT, 'more'), (TEXT, 'text')]
83 """
83 """
84
84
85 buffer = []
85 buffer = []
86 for token_class, token_text in tokens:
86 for token_class, token_text in tokens:
87 parts = token_text.split('\n')
87 parts = token_text.split('\n')
88 for part in parts[:-1]:
88 for part in parts[:-1]:
89 buffer.append((token_class, part))
89 buffer.append((token_class, part))
90 yield buffer
90 yield buffer
91 buffer = []
91 buffer = []
92
92
93 buffer.append((token_class, parts[-1]))
93 buffer.append((token_class, parts[-1]))
94
94
95 if buffer:
95 if buffer:
96 yield buffer
96 yield buffer
97
97
98
98
99 def filenode_as_annotated_lines_tokens(filenode):
99 def filenode_as_annotated_lines_tokens(filenode):
100 """
100 """
101 Take a file node and return a list of annotations => lines, if no annotation
101 Take a file node and return a list of annotations => lines, if no annotation
102 is found, it will be None.
102 is found, it will be None.
103
103
104 eg:
104 eg:
105
105
106 [
106 [
107 (annotation1, [
107 (annotation1, [
108 (1, line1_tokens_list),
108 (1, line1_tokens_list),
109 (2, line2_tokens_list),
109 (2, line2_tokens_list),
110 ]),
110 ]),
111 (annotation2, [
111 (annotation2, [
112 (3, line1_tokens_list),
112 (3, line1_tokens_list),
113 ]),
113 ]),
114 (None, [
114 (None, [
115 (4, line1_tokens_list),
115 (4, line1_tokens_list),
116 ]),
116 ]),
117 (annotation1, [
117 (annotation1, [
118 (5, line1_tokens_list),
118 (5, line1_tokens_list),
119 (6, line2_tokens_list),
119 (6, line2_tokens_list),
120 ])
120 ])
121 ]
121 ]
122 """
122 """
123
123
124 commit_cache = {} # cache commit_getter lookups
124 commit_cache = {} # cache commit_getter lookups
125
125
126 def _get_annotation(commit_id, commit_getter):
126 def _get_annotation(commit_id, commit_getter):
127 if commit_id not in commit_cache:
127 if commit_id not in commit_cache:
128 commit_cache[commit_id] = commit_getter()
128 commit_cache[commit_id] = commit_getter()
129 return commit_cache[commit_id]
129 return commit_cache[commit_id]
130
130
131 annotation_lookup = {
131 annotation_lookup = {
132 line_no: _get_annotation(commit_id, commit_getter)
132 line_no: _get_annotation(commit_id, commit_getter)
133 for line_no, commit_id, commit_getter, line_content
133 for line_no, commit_id, commit_getter, line_content
134 in filenode.annotate
134 in filenode.annotate
135 }
135 }
136
136
137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
138 for line_no, tokens
138 for line_no, tokens
139 in enumerate(filenode_as_lines_tokens(filenode), 1))
139 in enumerate(filenode_as_lines_tokens(filenode), 1))
140
140
141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
142
142
143 for annotation, group in grouped_annotations_lines:
143 for annotation, group in grouped_annotations_lines:
144 yield (
144 yield (
145 annotation, [(line_no, tokens)
145 annotation, [(line_no, tokens)
146 for (_, line_no, tokens) in group]
146 for (_, line_no, tokens) in group]
147 )
147 )
148
148
149
149
150 def render_tokenstream(tokenstream):
150 def render_tokenstream(tokenstream):
151 result = []
151 result = []
152 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
152 for token_class, token_ops_texts in rollup_tokenstream(tokenstream):
153
153
154 if token_class:
154 if token_class:
155 result.append(u'<span class="%s">' % token_class)
155 result.append(u'<span class="%s">' % token_class)
156 else:
156 else:
157 result.append(u'<span>')
157 result.append(u'<span>')
158
158
159 for op_tag, token_text in token_ops_texts:
159 for op_tag, token_text in token_ops_texts:
160
160
161 if op_tag:
161 if op_tag:
162 result.append(u'<%s>' % op_tag)
162 result.append(u'<%s>' % op_tag)
163
163
164 escaped_text = html_escape(token_text)
164 escaped_text = html_escape(token_text)
165
165
166 # TODO: dan: investigate showing hidden characters like space/nl/tab
166 # TODO: dan: investigate showing hidden characters like space/nl/tab
167 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
167 # escaped_text = escaped_text.replace(' ', '<sp> </sp>')
168 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
168 # escaped_text = escaped_text.replace('\n', '<nl>\n</nl>')
169 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
169 # escaped_text = escaped_text.replace('\t', '<tab>\t</tab>')
170
170
171 result.append(escaped_text)
171 result.append(escaped_text)
172
172
173 if op_tag:
173 if op_tag:
174 result.append(u'</%s>' % op_tag)
174 result.append(u'</%s>' % op_tag)
175
175
176 result.append(u'</span>')
176 result.append(u'</span>')
177
177
178 html = ''.join(result)
178 html = ''.join(result)
179 return html
179 return html
180
180
181
181
182 def rollup_tokenstream(tokenstream):
182 def rollup_tokenstream(tokenstream):
183 """
183 """
184 Group a token stream of the format:
184 Group a token stream of the format:
185
185
186 ('class', 'op', 'text')
186 ('class', 'op', 'text')
187 or
187 or
188 ('class', 'text')
188 ('class', 'text')
189
189
190 into
190 into
191
191
192 [('class1',
192 [('class1',
193 [('op1', 'text'),
193 [('op1', 'text'),
194 ('op2', 'text')]),
194 ('op2', 'text')]),
195 ('class2',
195 ('class2',
196 [('op3', 'text')])]
196 [('op3', 'text')])]
197
197
198 This is used to get the minimal tags necessary when
198 This is used to get the minimal tags necessary when
199 rendering to html eg for a token stream ie.
199 rendering to html eg for a token stream ie.
200
200
201 <span class="A"><ins>he</ins>llo</span>
201 <span class="A"><ins>he</ins>llo</span>
202 vs
202 vs
203 <span class="A"><ins>he</ins></span><span class="A">llo</span>
203 <span class="A"><ins>he</ins></span><span class="A">llo</span>
204
204
205 If a 2 tuple is passed in, the output op will be an empty string.
205 If a 2 tuple is passed in, the output op will be an empty string.
206
206
207 eg:
207 eg:
208
208
209 >>> rollup_tokenstream([('classA', '', 'h'),
209 >>> rollup_tokenstream([('classA', '', 'h'),
210 ('classA', 'del', 'ell'),
210 ('classA', 'del', 'ell'),
211 ('classA', '', 'o'),
211 ('classA', '', 'o'),
212 ('classB', '', ' '),
212 ('classB', '', ' '),
213 ('classA', '', 'the'),
213 ('classA', '', 'the'),
214 ('classA', '', 're'),
214 ('classA', '', 're'),
215 ])
215 ])
216
216
217 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
217 [('classA', [('', 'h'), ('del', 'ell'), ('', 'o')],
218 ('classB', [('', ' ')],
218 ('classB', [('', ' ')],
219 ('classA', [('', 'there')]]
219 ('classA', [('', 'there')]]
220
220
221 """
221 """
222 if tokenstream and len(tokenstream[0]) == 2:
222 if tokenstream and len(tokenstream[0]) == 2:
223 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
223 tokenstream = ((t[0], '', t[1]) for t in tokenstream)
224
224
225 result = []
225 result = []
226 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
226 for token_class, op_list in groupby(tokenstream, lambda t: t[0]):
227 ops = []
227 ops = []
228 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
228 for token_op, token_text_list in groupby(op_list, lambda o: o[1]):
229 text_buffer = []
229 text_buffer = []
230 for t_class, t_op, t_text in token_text_list:
230 for t_class, t_op, t_text in token_text_list:
231 text_buffer.append(t_text)
231 text_buffer.append(t_text)
232 ops.append((token_op, ''.join(text_buffer)))
232 ops.append((token_op, ''.join(text_buffer)))
233 result.append((token_class, ops))
233 result.append((token_class, ops))
234 return result
234 return result
235
235
236
236
237 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
237 def tokens_diff(old_tokens, new_tokens, use_diff_match_patch=True):
238 """
238 """
239 Converts a list of (token_class, token_text) tuples to a list of
239 Converts a list of (token_class, token_text) tuples to a list of
240 (token_class, token_op, token_text) tuples where token_op is one of
240 (token_class, token_op, token_text) tuples where token_op is one of
241 ('ins', 'del', '')
241 ('ins', 'del', '')
242
242
243 :param old_tokens: list of (token_class, token_text) tuples of old line
243 :param old_tokens: list of (token_class, token_text) tuples of old line
244 :param new_tokens: list of (token_class, token_text) tuples of new line
244 :param new_tokens: list of (token_class, token_text) tuples of new line
245 :param use_diff_match_patch: boolean, will use google's diff match patch
245 :param use_diff_match_patch: boolean, will use google's diff match patch
246 library which has options to 'smooth' out the character by character
246 library which has options to 'smooth' out the character by character
247 differences making nicer ins/del blocks
247 differences making nicer ins/del blocks
248 """
248 """
249
249
250 old_tokens_result = []
250 old_tokens_result = []
251 new_tokens_result = []
251 new_tokens_result = []
252
252
253 similarity = difflib.SequenceMatcher(None,
253 similarity = difflib.SequenceMatcher(None,
254 ''.join(token_text for token_class, token_text in old_tokens),
254 ''.join(token_text for token_class, token_text in old_tokens),
255 ''.join(token_text for token_class, token_text in new_tokens)
255 ''.join(token_text for token_class, token_text in new_tokens)
256 ).ratio()
256 ).ratio()
257
257
258 if similarity < 0.6: # return, the blocks are too different
258 if similarity < 0.6: # return, the blocks are too different
259 for token_class, token_text in old_tokens:
259 for token_class, token_text in old_tokens:
260 old_tokens_result.append((token_class, '', token_text))
260 old_tokens_result.append((token_class, '', token_text))
261 for token_class, token_text in new_tokens:
261 for token_class, token_text in new_tokens:
262 new_tokens_result.append((token_class, '', token_text))
262 new_tokens_result.append((token_class, '', token_text))
263 return old_tokens_result, new_tokens_result, similarity
263 return old_tokens_result, new_tokens_result, similarity
264
264
265 token_sequence_matcher = difflib.SequenceMatcher(None,
265 token_sequence_matcher = difflib.SequenceMatcher(None,
266 [x[1] for x in old_tokens],
266 [x[1] for x in old_tokens],
267 [x[1] for x in new_tokens])
267 [x[1] for x in new_tokens])
268
268
269 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
269 for tag, o1, o2, n1, n2 in token_sequence_matcher.get_opcodes():
270 # check the differences by token block types first to give a more
270 # check the differences by token block types first to give a more
271 # nicer "block" level replacement vs character diffs
271 # nicer "block" level replacement vs character diffs
272
272
273 if tag == 'equal':
273 if tag == 'equal':
274 for token_class, token_text in old_tokens[o1:o2]:
274 for token_class, token_text in old_tokens[o1:o2]:
275 old_tokens_result.append((token_class, '', token_text))
275 old_tokens_result.append((token_class, '', token_text))
276 for token_class, token_text in new_tokens[n1:n2]:
276 for token_class, token_text in new_tokens[n1:n2]:
277 new_tokens_result.append((token_class, '', token_text))
277 new_tokens_result.append((token_class, '', token_text))
278 elif tag == 'delete':
278 elif tag == 'delete':
279 for token_class, token_text in old_tokens[o1:o2]:
279 for token_class, token_text in old_tokens[o1:o2]:
280 old_tokens_result.append((token_class, 'del', token_text))
280 old_tokens_result.append((token_class, 'del', token_text))
281 elif tag == 'insert':
281 elif tag == 'insert':
282 for token_class, token_text in new_tokens[n1:n2]:
282 for token_class, token_text in new_tokens[n1:n2]:
283 new_tokens_result.append((token_class, 'ins', token_text))
283 new_tokens_result.append((token_class, 'ins', token_text))
284 elif tag == 'replace':
284 elif tag == 'replace':
285 # if same type token blocks must be replaced, do a diff on the
285 # if same type token blocks must be replaced, do a diff on the
286 # characters in the token blocks to show individual changes
286 # characters in the token blocks to show individual changes
287
287
288 old_char_tokens = []
288 old_char_tokens = []
289 new_char_tokens = []
289 new_char_tokens = []
290 for token_class, token_text in old_tokens[o1:o2]:
290 for token_class, token_text in old_tokens[o1:o2]:
291 for char in token_text:
291 for char in token_text:
292 old_char_tokens.append((token_class, char))
292 old_char_tokens.append((token_class, char))
293
293
294 for token_class, token_text in new_tokens[n1:n2]:
294 for token_class, token_text in new_tokens[n1:n2]:
295 for char in token_text:
295 for char in token_text:
296 new_char_tokens.append((token_class, char))
296 new_char_tokens.append((token_class, char))
297
297
298 old_string = ''.join([token_text for
298 old_string = ''.join([token_text for
299 token_class, token_text in old_char_tokens])
299 token_class, token_text in old_char_tokens])
300 new_string = ''.join([token_text for
300 new_string = ''.join([token_text for
301 token_class, token_text in new_char_tokens])
301 token_class, token_text in new_char_tokens])
302
302
303 char_sequence = difflib.SequenceMatcher(
303 char_sequence = difflib.SequenceMatcher(
304 None, old_string, new_string)
304 None, old_string, new_string)
305 copcodes = char_sequence.get_opcodes()
305 copcodes = char_sequence.get_opcodes()
306 obuffer, nbuffer = [], []
306 obuffer, nbuffer = [], []
307
307
308 if use_diff_match_patch:
308 if use_diff_match_patch:
309 dmp = diff_match_patch()
309 dmp = diff_match_patch()
310 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
310 dmp.Diff_EditCost = 11 # TODO: dan: extract this to a setting
311 reps = dmp.diff_main(old_string, new_string)
311 reps = dmp.diff_main(old_string, new_string)
312 dmp.diff_cleanupEfficiency(reps)
312 dmp.diff_cleanupEfficiency(reps)
313
313
314 a, b = 0, 0
314 a, b = 0, 0
315 for op, rep in reps:
315 for op, rep in reps:
316 l = len(rep)
316 l = len(rep)
317 if op == 0:
317 if op == 0:
318 for i, c in enumerate(rep):
318 for i, c in enumerate(rep):
319 obuffer.append((old_char_tokens[a+i][0], '', c))
319 obuffer.append((old_char_tokens[a+i][0], '', c))
320 nbuffer.append((new_char_tokens[b+i][0], '', c))
320 nbuffer.append((new_char_tokens[b+i][0], '', c))
321 a += l
321 a += l
322 b += l
322 b += l
323 elif op == -1:
323 elif op == -1:
324 for i, c in enumerate(rep):
324 for i, c in enumerate(rep):
325 obuffer.append((old_char_tokens[a+i][0], 'del', c))
325 obuffer.append((old_char_tokens[a+i][0], 'del', c))
326 a += l
326 a += l
327 elif op == 1:
327 elif op == 1:
328 for i, c in enumerate(rep):
328 for i, c in enumerate(rep):
329 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
329 nbuffer.append((new_char_tokens[b+i][0], 'ins', c))
330 b += l
330 b += l
331 else:
331 else:
332 for ctag, co1, co2, cn1, cn2 in copcodes:
332 for ctag, co1, co2, cn1, cn2 in copcodes:
333 if ctag == 'equal':
333 if ctag == 'equal':
334 for token_class, token_text in old_char_tokens[co1:co2]:
334 for token_class, token_text in old_char_tokens[co1:co2]:
335 obuffer.append((token_class, '', token_text))
335 obuffer.append((token_class, '', token_text))
336 for token_class, token_text in new_char_tokens[cn1:cn2]:
336 for token_class, token_text in new_char_tokens[cn1:cn2]:
337 nbuffer.append((token_class, '', token_text))
337 nbuffer.append((token_class, '', token_text))
338 elif ctag == 'delete':
338 elif ctag == 'delete':
339 for token_class, token_text in old_char_tokens[co1:co2]:
339 for token_class, token_text in old_char_tokens[co1:co2]:
340 obuffer.append((token_class, 'del', token_text))
340 obuffer.append((token_class, 'del', token_text))
341 elif ctag == 'insert':
341 elif ctag == 'insert':
342 for token_class, token_text in new_char_tokens[cn1:cn2]:
342 for token_class, token_text in new_char_tokens[cn1:cn2]:
343 nbuffer.append((token_class, 'ins', token_text))
343 nbuffer.append((token_class, 'ins', token_text))
344 elif ctag == 'replace':
344 elif ctag == 'replace':
345 for token_class, token_text in old_char_tokens[co1:co2]:
345 for token_class, token_text in old_char_tokens[co1:co2]:
346 obuffer.append((token_class, 'del', token_text))
346 obuffer.append((token_class, 'del', token_text))
347 for token_class, token_text in new_char_tokens[cn1:cn2]:
347 for token_class, token_text in new_char_tokens[cn1:cn2]:
348 nbuffer.append((token_class, 'ins', token_text))
348 nbuffer.append((token_class, 'ins', token_text))
349
349
350 old_tokens_result.extend(obuffer)
350 old_tokens_result.extend(obuffer)
351 new_tokens_result.extend(nbuffer)
351 new_tokens_result.extend(nbuffer)
352
352
353 return old_tokens_result, new_tokens_result, similarity
353 return old_tokens_result, new_tokens_result, similarity
354
354
355
355
356 def diffset_node_getter(commit):
356 def diffset_node_getter(commit):
357 def get_node(fname):
357 def get_node(fname):
358 try:
358 try:
359 return commit.get_node(fname)
359 return commit.get_node(fname)
360 except NodeDoesNotExistError:
360 except NodeDoesNotExistError:
361 return None
361 return None
362
362
363 return get_node
363 return get_node
364
364
365
365
366 class DiffSet(object):
366 class DiffSet(object):
367 """
367 """
368 An object for parsing the diff result from diffs.DiffProcessor and
368 An object for parsing the diff result from diffs.DiffProcessor and
369 adding highlighting, side by side/unified renderings and line diffs
369 adding highlighting, side by side/unified renderings and line diffs
370 """
370 """
371
371
372 HL_REAL = 'REAL' # highlights using original file, slow
372 HL_REAL = 'REAL' # highlights using original file, slow
373 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
373 HL_FAST = 'FAST' # highlights using just the line, fast but not correct
374 # in the case of multiline code
374 # in the case of multiline code
375 HL_NONE = 'NONE' # no highlighting, fastest
375 HL_NONE = 'NONE' # no highlighting, fastest
376
376
377 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
377 def __init__(self, highlight_mode=HL_REAL, repo_name=None,
378 source_repo_name=None,
378 source_repo_name=None,
379 source_node_getter=lambda filename: None,
379 source_node_getter=lambda filename: None,
380 target_repo_name=None,
380 target_node_getter=lambda filename: None,
381 target_node_getter=lambda filename: None,
381 source_nodes=None, target_nodes=None,
382 source_nodes=None, target_nodes=None,
382 # files over this size will use fast highlighting
383 # files over this size will use fast highlighting
383 max_file_size_limit=150 * 1024,
384 max_file_size_limit=150 * 1024,
384 ):
385 ):
385
386
386 self.highlight_mode = highlight_mode
387 self.highlight_mode = highlight_mode
387 self.highlighted_filenodes = {}
388 self.highlighted_filenodes = {}
388 self.source_node_getter = source_node_getter
389 self.source_node_getter = source_node_getter
389 self.target_node_getter = target_node_getter
390 self.target_node_getter = target_node_getter
390 self.source_nodes = source_nodes or {}
391 self.source_nodes = source_nodes or {}
391 self.target_nodes = target_nodes or {}
392 self.target_nodes = target_nodes or {}
392 self.repo_name = repo_name
393 self.repo_name = repo_name
394 self.target_repo_name = target_repo_name or repo_name
393 self.source_repo_name = source_repo_name or repo_name
395 self.source_repo_name = source_repo_name or repo_name
394 self.max_file_size_limit = max_file_size_limit
396 self.max_file_size_limit = max_file_size_limit
395
397
396 def render_patchset(self, patchset, source_ref=None, target_ref=None):
398 def render_patchset(self, patchset, source_ref=None, target_ref=None):
397 diffset = AttributeDict(dict(
399 diffset = AttributeDict(dict(
398 lines_added=0,
400 lines_added=0,
399 lines_deleted=0,
401 lines_deleted=0,
400 changed_files=0,
402 changed_files=0,
401 files=[],
403 files=[],
402 file_stats={},
404 file_stats={},
403 limited_diff=isinstance(patchset, LimitedDiffContainer),
405 limited_diff=isinstance(patchset, LimitedDiffContainer),
404 repo_name=self.repo_name,
406 repo_name=self.repo_name,
407 target_repo_name=self.target_repo_name,
405 source_repo_name=self.source_repo_name,
408 source_repo_name=self.source_repo_name,
406 source_ref=source_ref,
409 source_ref=source_ref,
407 target_ref=target_ref,
410 target_ref=target_ref,
408 ))
411 ))
409 for patch in patchset:
412 for patch in patchset:
410 diffset.file_stats[patch['filename']] = patch['stats']
413 diffset.file_stats[patch['filename']] = patch['stats']
411 filediff = self.render_patch(patch)
414 filediff = self.render_patch(patch)
412 filediff.diffset = StrictAttributeDict(dict(
415 filediff.diffset = StrictAttributeDict(dict(
413 source_ref=diffset.source_ref,
416 source_ref=diffset.source_ref,
414 target_ref=diffset.target_ref,
417 target_ref=diffset.target_ref,
415 repo_name=diffset.repo_name,
418 repo_name=diffset.repo_name,
416 source_repo_name=diffset.source_repo_name,
419 source_repo_name=diffset.source_repo_name,
420 target_repo_name=diffset.target_repo_name,
417 ))
421 ))
418 diffset.files.append(filediff)
422 diffset.files.append(filediff)
419 diffset.changed_files += 1
423 diffset.changed_files += 1
420 if not patch['stats']['binary']:
424 if not patch['stats']['binary']:
421 diffset.lines_added += patch['stats']['added']
425 diffset.lines_added += patch['stats']['added']
422 diffset.lines_deleted += patch['stats']['deleted']
426 diffset.lines_deleted += patch['stats']['deleted']
423
427
424 return diffset
428 return diffset
425
429
426 _lexer_cache = {}
430 _lexer_cache = {}
427
431
428 def _get_lexer_for_filename(self, filename, filenode=None):
432 def _get_lexer_for_filename(self, filename, filenode=None):
429 # cached because we might need to call it twice for source/target
433 # cached because we might need to call it twice for source/target
430 if filename not in self._lexer_cache:
434 if filename not in self._lexer_cache:
431 if filenode:
435 if filenode:
432 lexer = filenode.lexer
436 lexer = filenode.lexer
433 extension = filenode.extension
437 extension = filenode.extension
434 else:
438 else:
435 lexer = FileNode.get_lexer(filename=filename)
439 lexer = FileNode.get_lexer(filename=filename)
436 extension = filename.split('.')[-1]
440 extension = filename.split('.')[-1]
437
441
438 lexer = get_custom_lexer(extension) or lexer
442 lexer = get_custom_lexer(extension) or lexer
439 self._lexer_cache[filename] = lexer
443 self._lexer_cache[filename] = lexer
440 return self._lexer_cache[filename]
444 return self._lexer_cache[filename]
441
445
442 def render_patch(self, patch):
446 def render_patch(self, patch):
443 log.debug('rendering diff for %r', patch['filename'])
447 log.debug('rendering diff for %r', patch['filename'])
444
448
445 source_filename = patch['original_filename']
449 source_filename = patch['original_filename']
446 target_filename = patch['filename']
450 target_filename = patch['filename']
447
451
448 source_lexer = plain_text_lexer
452 source_lexer = plain_text_lexer
449 target_lexer = plain_text_lexer
453 target_lexer = plain_text_lexer
450
454
451 if not patch['stats']['binary']:
455 if not patch['stats']['binary']:
452 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
456 node_hl_mode = self.HL_NONE if patch['chunks'] == [] else None
453 hl_mode = node_hl_mode or self.highlight_mode
457 hl_mode = node_hl_mode or self.highlight_mode
454
458
455 if hl_mode == self.HL_REAL:
459 if hl_mode == self.HL_REAL:
456 if (source_filename and patch['operation'] in ('D', 'M')
460 if (source_filename and patch['operation'] in ('D', 'M')
457 and source_filename not in self.source_nodes):
461 and source_filename not in self.source_nodes):
458 self.source_nodes[source_filename] = (
462 self.source_nodes[source_filename] = (
459 self.source_node_getter(source_filename))
463 self.source_node_getter(source_filename))
460
464
461 if (target_filename and patch['operation'] in ('A', 'M')
465 if (target_filename and patch['operation'] in ('A', 'M')
462 and target_filename not in self.target_nodes):
466 and target_filename not in self.target_nodes):
463 self.target_nodes[target_filename] = (
467 self.target_nodes[target_filename] = (
464 self.target_node_getter(target_filename))
468 self.target_node_getter(target_filename))
465
469
466 elif hl_mode == self.HL_FAST:
470 elif hl_mode == self.HL_FAST:
467 source_lexer = self._get_lexer_for_filename(source_filename)
471 source_lexer = self._get_lexer_for_filename(source_filename)
468 target_lexer = self._get_lexer_for_filename(target_filename)
472 target_lexer = self._get_lexer_for_filename(target_filename)
469
473
470 source_file = self.source_nodes.get(source_filename, source_filename)
474 source_file = self.source_nodes.get(source_filename, source_filename)
471 target_file = self.target_nodes.get(target_filename, target_filename)
475 target_file = self.target_nodes.get(target_filename, target_filename)
472 raw_id_uid = ''
476 raw_id_uid = ''
473 if self.source_nodes.get(source_filename):
477 if self.source_nodes.get(source_filename):
474 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
478 raw_id_uid = self.source_nodes[source_filename].commit.raw_id
475
479
476 if not raw_id_uid and self.target_nodes.get(target_filename):
480 if not raw_id_uid and self.target_nodes.get(target_filename):
477 # in case this is a new file we only have it in target
481 # in case this is a new file we only have it in target
478 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
482 raw_id_uid = self.target_nodes[target_filename].commit.raw_id
479
483
480 source_filenode, target_filenode = None, None
484 source_filenode, target_filenode = None, None
481
485
482 # TODO: dan: FileNode.lexer works on the content of the file - which
486 # TODO: dan: FileNode.lexer works on the content of the file - which
483 # can be slow - issue #4289 explains a lexer clean up - which once
487 # can be slow - issue #4289 explains a lexer clean up - which once
484 # done can allow caching a lexer for a filenode to avoid the file lookup
488 # done can allow caching a lexer for a filenode to avoid the file lookup
485 if isinstance(source_file, FileNode):
489 if isinstance(source_file, FileNode):
486 source_filenode = source_file
490 source_filenode = source_file
487 #source_lexer = source_file.lexer
491 #source_lexer = source_file.lexer
488 source_lexer = self._get_lexer_for_filename(source_filename)
492 source_lexer = self._get_lexer_for_filename(source_filename)
489 source_file.lexer = source_lexer
493 source_file.lexer = source_lexer
490
494
491 if isinstance(target_file, FileNode):
495 if isinstance(target_file, FileNode):
492 target_filenode = target_file
496 target_filenode = target_file
493 #target_lexer = target_file.lexer
497 #target_lexer = target_file.lexer
494 target_lexer = self._get_lexer_for_filename(target_filename)
498 target_lexer = self._get_lexer_for_filename(target_filename)
495 target_file.lexer = target_lexer
499 target_file.lexer = target_lexer
496
500
497 source_file_path, target_file_path = None, None
501 source_file_path, target_file_path = None, None
498
502
499 if source_filename != '/dev/null':
503 if source_filename != '/dev/null':
500 source_file_path = source_filename
504 source_file_path = source_filename
501 if target_filename != '/dev/null':
505 if target_filename != '/dev/null':
502 target_file_path = target_filename
506 target_file_path = target_filename
503
507
504 source_file_type = source_lexer.name
508 source_file_type = source_lexer.name
505 target_file_type = target_lexer.name
509 target_file_type = target_lexer.name
506
510
507 filediff = AttributeDict({
511 filediff = AttributeDict({
508 'source_file_path': source_file_path,
512 'source_file_path': source_file_path,
509 'target_file_path': target_file_path,
513 'target_file_path': target_file_path,
510 'source_filenode': source_filenode,
514 'source_filenode': source_filenode,
511 'target_filenode': target_filenode,
515 'target_filenode': target_filenode,
512 'source_file_type': target_file_type,
516 'source_file_type': target_file_type,
513 'target_file_type': source_file_type,
517 'target_file_type': source_file_type,
514 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
518 'patch': {'filename': patch['filename'], 'stats': patch['stats']},
515 'operation': patch['operation'],
519 'operation': patch['operation'],
516 'source_mode': patch['stats']['old_mode'],
520 'source_mode': patch['stats']['old_mode'],
517 'target_mode': patch['stats']['new_mode'],
521 'target_mode': patch['stats']['new_mode'],
518 'limited_diff': isinstance(patch, LimitedDiffContainer),
522 'limited_diff': isinstance(patch, LimitedDiffContainer),
519 'hunks': [],
523 'hunks': [],
520 'hunk_ops': None,
524 'hunk_ops': None,
521 'diffset': self,
525 'diffset': self,
522 'raw_id': raw_id_uid,
526 'raw_id': raw_id_uid,
523 })
527 })
524
528
525 file_chunks = patch['chunks'][1:]
529 file_chunks = patch['chunks'][1:]
526 for hunk in file_chunks:
530 for hunk in file_chunks:
527 hunkbit = self.parse_hunk(hunk, source_file, target_file)
531 hunkbit = self.parse_hunk(hunk, source_file, target_file)
528 hunkbit.source_file_path = source_file_path
532 hunkbit.source_file_path = source_file_path
529 hunkbit.target_file_path = target_file_path
533 hunkbit.target_file_path = target_file_path
530 filediff.hunks.append(hunkbit)
534 filediff.hunks.append(hunkbit)
531
535
532 # Simulate hunk on OPS type line which doesn't really contain any diff
536 # Simulate hunk on OPS type line which doesn't really contain any diff
533 # this allows commenting on those
537 # this allows commenting on those
534 if not file_chunks:
538 if not file_chunks:
535 actions = []
539 actions = []
536 for op_id, op_text in filediff.patch['stats']['ops'].items():
540 for op_id, op_text in filediff.patch['stats']['ops'].items():
537 if op_id == DEL_FILENODE:
541 if op_id == DEL_FILENODE:
538 actions.append(u'file was removed')
542 actions.append(u'file was removed')
539 elif op_id == BIN_FILENODE:
543 elif op_id == BIN_FILENODE:
540 actions.append(u'binary diff hidden')
544 actions.append(u'binary diff hidden')
541 else:
545 else:
542 actions.append(safe_unicode(op_text))
546 actions.append(safe_unicode(op_text))
543 action_line = u'NO CONTENT: ' + \
547 action_line = u'NO CONTENT: ' + \
544 u', '.join(actions) or u'UNDEFINED_ACTION'
548 u', '.join(actions) or u'UNDEFINED_ACTION'
545
549
546 hunk_ops = {'source_length': 0, 'source_start': 0,
550 hunk_ops = {'source_length': 0, 'source_start': 0,
547 'lines': [
551 'lines': [
548 {'new_lineno': 0, 'old_lineno': 1,
552 {'new_lineno': 0, 'old_lineno': 1,
549 'action': 'unmod-no-hl', 'line': action_line}
553 'action': 'unmod-no-hl', 'line': action_line}
550 ],
554 ],
551 'section_header': u'', 'target_start': 1, 'target_length': 1}
555 'section_header': u'', 'target_start': 1, 'target_length': 1}
552
556
553 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
557 hunkbit = self.parse_hunk(hunk_ops, source_file, target_file)
554 hunkbit.source_file_path = source_file_path
558 hunkbit.source_file_path = source_file_path
555 hunkbit.target_file_path = target_file_path
559 hunkbit.target_file_path = target_file_path
556 filediff.hunk_ops = hunkbit
560 filediff.hunk_ops = hunkbit
557 return filediff
561 return filediff
558
562
559 def parse_hunk(self, hunk, source_file, target_file):
563 def parse_hunk(self, hunk, source_file, target_file):
560 result = AttributeDict(dict(
564 result = AttributeDict(dict(
561 source_start=hunk['source_start'],
565 source_start=hunk['source_start'],
562 source_length=hunk['source_length'],
566 source_length=hunk['source_length'],
563 target_start=hunk['target_start'],
567 target_start=hunk['target_start'],
564 target_length=hunk['target_length'],
568 target_length=hunk['target_length'],
565 section_header=hunk['section_header'],
569 section_header=hunk['section_header'],
566 lines=[],
570 lines=[],
567 ))
571 ))
568 before, after = [], []
572 before, after = [], []
569
573
570 for line in hunk['lines']:
574 for line in hunk['lines']:
571 if line['action'] in ['unmod', 'unmod-no-hl']:
575 if line['action'] in ['unmod', 'unmod-no-hl']:
572 no_hl = line['action'] == 'unmod-no-hl'
576 no_hl = line['action'] == 'unmod-no-hl'
573 result.lines.extend(
577 result.lines.extend(
574 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
578 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
575 after.append(line)
579 after.append(line)
576 before.append(line)
580 before.append(line)
577 elif line['action'] == 'add':
581 elif line['action'] == 'add':
578 after.append(line)
582 after.append(line)
579 elif line['action'] == 'del':
583 elif line['action'] == 'del':
580 before.append(line)
584 before.append(line)
581 elif line['action'] == 'old-no-nl':
585 elif line['action'] == 'old-no-nl':
582 before.append(line)
586 before.append(line)
583 elif line['action'] == 'new-no-nl':
587 elif line['action'] == 'new-no-nl':
584 after.append(line)
588 after.append(line)
585
589
586 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
590 all_actions = [x['action'] for x in after] + [x['action'] for x in before]
587 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
591 no_hl = {x for x in all_actions} == {'unmod-no-hl'}
588 result.lines.extend(
592 result.lines.extend(
589 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
593 self.parse_lines(before, after, source_file, target_file, no_hl=no_hl))
590 # NOTE(marcink): we must keep list() call here so we can cache the result...
594 # NOTE(marcink): we must keep list() call here so we can cache the result...
591 result.unified = list(self.as_unified(result.lines))
595 result.unified = list(self.as_unified(result.lines))
592 result.sideside = result.lines
596 result.sideside = result.lines
593
597
594 return result
598 return result
595
599
596 def parse_lines(self, before_lines, after_lines, source_file, target_file,
600 def parse_lines(self, before_lines, after_lines, source_file, target_file,
597 no_hl=False):
601 no_hl=False):
598 # TODO: dan: investigate doing the diff comparison and fast highlighting
602 # TODO: dan: investigate doing the diff comparison and fast highlighting
599 # on the entire before and after buffered block lines rather than by
603 # on the entire before and after buffered block lines rather than by
600 # line, this means we can get better 'fast' highlighting if the context
604 # line, this means we can get better 'fast' highlighting if the context
601 # allows it - eg.
605 # allows it - eg.
602 # line 4: """
606 # line 4: """
603 # line 5: this gets highlighted as a string
607 # line 5: this gets highlighted as a string
604 # line 6: """
608 # line 6: """
605
609
606 lines = []
610 lines = []
607
611
608 before_newline = AttributeDict()
612 before_newline = AttributeDict()
609 after_newline = AttributeDict()
613 after_newline = AttributeDict()
610 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
614 if before_lines and before_lines[-1]['action'] == 'old-no-nl':
611 before_newline_line = before_lines.pop(-1)
615 before_newline_line = before_lines.pop(-1)
612 before_newline.content = '\n {}'.format(
616 before_newline.content = '\n {}'.format(
613 render_tokenstream(
617 render_tokenstream(
614 [(x[0], '', x[1])
618 [(x[0], '', x[1])
615 for x in [('nonl', before_newline_line['line'])]]))
619 for x in [('nonl', before_newline_line['line'])]]))
616
620
617 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
621 if after_lines and after_lines[-1]['action'] == 'new-no-nl':
618 after_newline_line = after_lines.pop(-1)
622 after_newline_line = after_lines.pop(-1)
619 after_newline.content = '\n {}'.format(
623 after_newline.content = '\n {}'.format(
620 render_tokenstream(
624 render_tokenstream(
621 [(x[0], '', x[1])
625 [(x[0], '', x[1])
622 for x in [('nonl', after_newline_line['line'])]]))
626 for x in [('nonl', after_newline_line['line'])]]))
623
627
624 while before_lines or after_lines:
628 while before_lines or after_lines:
625 before, after = None, None
629 before, after = None, None
626 before_tokens, after_tokens = None, None
630 before_tokens, after_tokens = None, None
627
631
628 if before_lines:
632 if before_lines:
629 before = before_lines.pop(0)
633 before = before_lines.pop(0)
630 if after_lines:
634 if after_lines:
631 after = after_lines.pop(0)
635 after = after_lines.pop(0)
632
636
633 original = AttributeDict()
637 original = AttributeDict()
634 modified = AttributeDict()
638 modified = AttributeDict()
635
639
636 if before:
640 if before:
637 if before['action'] == 'old-no-nl':
641 if before['action'] == 'old-no-nl':
638 before_tokens = [('nonl', before['line'])]
642 before_tokens = [('nonl', before['line'])]
639 else:
643 else:
640 before_tokens = self.get_line_tokens(
644 before_tokens = self.get_line_tokens(
641 line_text=before['line'], line_number=before['old_lineno'],
645 line_text=before['line'], line_number=before['old_lineno'],
642 input_file=source_file, no_hl=no_hl)
646 input_file=source_file, no_hl=no_hl)
643 original.lineno = before['old_lineno']
647 original.lineno = before['old_lineno']
644 original.content = before['line']
648 original.content = before['line']
645 original.action = self.action_to_op(before['action'])
649 original.action = self.action_to_op(before['action'])
646
650
647 original.get_comment_args = (
651 original.get_comment_args = (
648 source_file, 'o', before['old_lineno'])
652 source_file, 'o', before['old_lineno'])
649
653
650 if after:
654 if after:
651 if after['action'] == 'new-no-nl':
655 if after['action'] == 'new-no-nl':
652 after_tokens = [('nonl', after['line'])]
656 after_tokens = [('nonl', after['line'])]
653 else:
657 else:
654 after_tokens = self.get_line_tokens(
658 after_tokens = self.get_line_tokens(
655 line_text=after['line'], line_number=after['new_lineno'],
659 line_text=after['line'], line_number=after['new_lineno'],
656 input_file=target_file, no_hl=no_hl)
660 input_file=target_file, no_hl=no_hl)
657 modified.lineno = after['new_lineno']
661 modified.lineno = after['new_lineno']
658 modified.content = after['line']
662 modified.content = after['line']
659 modified.action = self.action_to_op(after['action'])
663 modified.action = self.action_to_op(after['action'])
660
664
661 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
665 modified.get_comment_args = (target_file, 'n', after['new_lineno'])
662
666
663 # diff the lines
667 # diff the lines
664 if before_tokens and after_tokens:
668 if before_tokens and after_tokens:
665 o_tokens, m_tokens, similarity = tokens_diff(
669 o_tokens, m_tokens, similarity = tokens_diff(
666 before_tokens, after_tokens)
670 before_tokens, after_tokens)
667 original.content = render_tokenstream(o_tokens)
671 original.content = render_tokenstream(o_tokens)
668 modified.content = render_tokenstream(m_tokens)
672 modified.content = render_tokenstream(m_tokens)
669 elif before_tokens:
673 elif before_tokens:
670 original.content = render_tokenstream(
674 original.content = render_tokenstream(
671 [(x[0], '', x[1]) for x in before_tokens])
675 [(x[0], '', x[1]) for x in before_tokens])
672 elif after_tokens:
676 elif after_tokens:
673 modified.content = render_tokenstream(
677 modified.content = render_tokenstream(
674 [(x[0], '', x[1]) for x in after_tokens])
678 [(x[0], '', x[1]) for x in after_tokens])
675
679
676 if not before_lines and before_newline:
680 if not before_lines and before_newline:
677 original.content += before_newline.content
681 original.content += before_newline.content
678 before_newline = None
682 before_newline = None
679 if not after_lines and after_newline:
683 if not after_lines and after_newline:
680 modified.content += after_newline.content
684 modified.content += after_newline.content
681 after_newline = None
685 after_newline = None
682
686
683 lines.append(AttributeDict({
687 lines.append(AttributeDict({
684 'original': original,
688 'original': original,
685 'modified': modified,
689 'modified': modified,
686 }))
690 }))
687
691
688 return lines
692 return lines
689
693
690 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False):
694 def get_line_tokens(self, line_text, line_number, input_file=None, no_hl=False):
691 filenode = None
695 filenode = None
692 filename = None
696 filename = None
693
697
694 if isinstance(input_file, basestring):
698 if isinstance(input_file, basestring):
695 filename = input_file
699 filename = input_file
696 elif isinstance(input_file, FileNode):
700 elif isinstance(input_file, FileNode):
697 filenode = input_file
701 filenode = input_file
698 filename = input_file.unicode_path
702 filename = input_file.unicode_path
699
703
700 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
704 hl_mode = self.HL_NONE if no_hl else self.highlight_mode
701 if hl_mode == self.HL_REAL and filenode:
705 if hl_mode == self.HL_REAL and filenode:
702 lexer = self._get_lexer_for_filename(filename)
706 lexer = self._get_lexer_for_filename(filename)
703 file_size_allowed = input_file.size < self.max_file_size_limit
707 file_size_allowed = input_file.size < self.max_file_size_limit
704 if line_number and file_size_allowed:
708 if line_number and file_size_allowed:
705 return self.get_tokenized_filenode_line(
709 return self.get_tokenized_filenode_line(
706 input_file, line_number, lexer)
710 input_file, line_number, lexer)
707
711
708 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
712 if hl_mode in (self.HL_REAL, self.HL_FAST) and filename:
709 lexer = self._get_lexer_for_filename(filename)
713 lexer = self._get_lexer_for_filename(filename)
710 return list(tokenize_string(line_text, lexer))
714 return list(tokenize_string(line_text, lexer))
711
715
712 return list(tokenize_string(line_text, plain_text_lexer))
716 return list(tokenize_string(line_text, plain_text_lexer))
713
717
714 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
718 def get_tokenized_filenode_line(self, filenode, line_number, lexer=None):
715
719
716 if filenode not in self.highlighted_filenodes:
720 if filenode not in self.highlighted_filenodes:
717 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
721 tokenized_lines = filenode_as_lines_tokens(filenode, lexer)
718 self.highlighted_filenodes[filenode] = tokenized_lines
722 self.highlighted_filenodes[filenode] = tokenized_lines
719 return self.highlighted_filenodes[filenode][line_number - 1]
723 return self.highlighted_filenodes[filenode][line_number - 1]
720
724
721 def action_to_op(self, action):
725 def action_to_op(self, action):
722 return {
726 return {
723 'add': '+',
727 'add': '+',
724 'del': '-',
728 'del': '-',
725 'unmod': ' ',
729 'unmod': ' ',
726 'unmod-no-hl': ' ',
730 'unmod-no-hl': ' ',
727 'old-no-nl': ' ',
731 'old-no-nl': ' ',
728 'new-no-nl': ' ',
732 'new-no-nl': ' ',
729 }.get(action, action)
733 }.get(action, action)
730
734
731 def as_unified(self, lines):
735 def as_unified(self, lines):
732 """
736 """
733 Return a generator that yields the lines of a diff in unified order
737 Return a generator that yields the lines of a diff in unified order
734 """
738 """
735 def generator():
739 def generator():
736 buf = []
740 buf = []
737 for line in lines:
741 for line in lines:
738
742
739 if buf and not line.original or line.original.action == ' ':
743 if buf and not line.original or line.original.action == ' ':
740 for b in buf:
744 for b in buf:
741 yield b
745 yield b
742 buf = []
746 buf = []
743
747
744 if line.original:
748 if line.original:
745 if line.original.action == ' ':
749 if line.original.action == ' ':
746 yield (line.original.lineno, line.modified.lineno,
750 yield (line.original.lineno, line.modified.lineno,
747 line.original.action, line.original.content,
751 line.original.action, line.original.content,
748 line.original.get_comment_args)
752 line.original.get_comment_args)
749 continue
753 continue
750
754
751 if line.original.action == '-':
755 if line.original.action == '-':
752 yield (line.original.lineno, None,
756 yield (line.original.lineno, None,
753 line.original.action, line.original.content,
757 line.original.action, line.original.content,
754 line.original.get_comment_args)
758 line.original.get_comment_args)
755
759
756 if line.modified.action == '+':
760 if line.modified.action == '+':
757 buf.append((
761 buf.append((
758 None, line.modified.lineno,
762 None, line.modified.lineno,
759 line.modified.action, line.modified.content,
763 line.modified.action, line.modified.content,
760 line.modified.get_comment_args))
764 line.modified.get_comment_args))
761 continue
765 continue
762
766
763 if line.modified:
767 if line.modified:
764 yield (None, line.modified.lineno,
768 yield (None, line.modified.lineno,
765 line.modified.action, line.modified.content,
769 line.modified.action, line.modified.content,
766 line.modified.get_comment_args)
770 line.modified.get_comment_args)
767
771
768 for b in buf:
772 for b in buf:
769 yield b
773 yield b
770
774
771 return generator()
775 return generator()
@@ -1,1016 +1,1022 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
447 %if filediff.diffset.source_ref:
448
449 ## FILE BEFORE CHANGES
447 %if filediff.operation in ['D', 'M']:
450 %if filediff.operation in ['D', 'M']:
448 <a
451 <a
449 class="tooltip"
452 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)}"
453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
451 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
452 >
455 >
453 ${_('Show file before')}
456 ${_('Show file before')}
454 </a> |
457 </a> |
455 %else:
458 %else:
456 <span
459 <span
457 class="tooltip"
460 class="tooltip"
458 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
459 >
462 >
460 ${_('Show file before')}
463 ${_('Show file before')}
461 </span> |
464 </span> |
462 %endif
465 %endif
466
467 ## FILE AFTER CHANGES
463 %if filediff.operation in ['A', 'M']:
468 %if filediff.operation in ['A', 'M']:
464 <a
469 <a
465 class="tooltip"
470 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)}"
471 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]})}"
472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
468 >
473 >
469 ${_('Show file after')}
474 ${_('Show file after')}
470 </a>
475 </a>
471 %else:
476 %else:
472 <span
477 <span
473 class="tooltip"
478 class="tooltip"
474 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
475 >
480 >
476 ${_('Show file after')}
481 ${_('Show file after')}
477 </span>
482 </span>
478 %endif
483 %endif
479
484
480 % if use_comments:
485 % if use_comments:
481 |
486 |
482 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
487 <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>
488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
484 </a>
489 </a>
485 % endif
490 % endif
486
491
487 %endif
492 %endif
493
488 </div>
494 </div>
489 </%def>
495 </%def>
490
496
491
497
492 <%def name="inline_comments_container(comments, inline_comments)">
498 <%def name="inline_comments_container(comments, inline_comments)">
493 <div class="inline-comments">
499 <div class="inline-comments">
494 %for comment in comments:
500 %for comment in comments:
495 ${commentblock.comment_block(comment, inline=True)}
501 ${commentblock.comment_block(comment, inline=True)}
496 %endfor
502 %endfor
497 % if comments and comments[-1].outdated:
503 % if comments and comments[-1].outdated:
498 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
499 style="display: none;}">
505 style="display: none;}">
500 ${_('Add another comment')}
506 ${_('Add another comment')}
501 </span>
507 </span>
502 % else:
508 % else:
503 <span onclick="return Rhodecode.comments.createComment(this)"
509 <span onclick="return Rhodecode.comments.createComment(this)"
504 class="btn btn-secondary cb-comment-add-button">
510 class="btn btn-secondary cb-comment-add-button">
505 ${_('Add another comment')}
511 ${_('Add another comment')}
506 </span>
512 </span>
507 % endif
513 % endif
508
514
509 </div>
515 </div>
510 </%def>
516 </%def>
511
517
512 <%!
518 <%!
513 def get_comments_for(diff_type, comments, filename, line_version, line_number):
519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
514 if hasattr(filename, 'unicode_path'):
520 if hasattr(filename, 'unicode_path'):
515 filename = filename.unicode_path
521 filename = filename.unicode_path
516
522
517 if not isinstance(filename, basestring):
523 if not isinstance(filename, basestring):
518 return None
524 return None
519
525
520 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
521
527
522 if comments and filename in comments:
528 if comments and filename in comments:
523 file_comments = comments[filename]
529 file_comments = comments[filename]
524 if line_key in file_comments:
530 if line_key in file_comments:
525 data = file_comments.pop(line_key)
531 data = file_comments.pop(line_key)
526 return data
532 return data
527 %>
533 %>
528
534
529 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
530 %for i, line in enumerate(hunk.sideside):
536 %for i, line in enumerate(hunk.sideside):
531 <%
537 <%
532 old_line_anchor, new_line_anchor = None, None
538 old_line_anchor, new_line_anchor = None, None
533
539
534 if line.original.lineno:
540 if line.original.lineno:
535 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
536 if line.modified.lineno:
542 if line.modified.lineno:
537 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
538 %>
544 %>
539
545
540 <tr class="cb-line">
546 <tr class="cb-line">
541 <td class="cb-data ${action_class(line.original.action)}"
547 <td class="cb-data ${action_class(line.original.action)}"
542 data-line-no="${line.original.lineno}"
548 data-line-no="${line.original.lineno}"
543 >
549 >
544 <div>
550 <div>
545
551
546 <% line_old_comments = None %>
552 <% line_old_comments = None %>
547 %if line.original.get_comment_args:
553 %if line.original.get_comment_args:
548 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
549 %endif
555 %endif
550 %if line_old_comments:
556 %if line_old_comments:
551 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
552 % if has_outdated:
558 % if has_outdated:
553 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
554 % else:
560 % else:
555 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
556 % endif
562 % endif
557 %endif
563 %endif
558 </div>
564 </div>
559 </td>
565 </td>
560 <td class="cb-lineno ${action_class(line.original.action)}"
566 <td class="cb-lineno ${action_class(line.original.action)}"
561 data-line-no="${line.original.lineno}"
567 data-line-no="${line.original.lineno}"
562 %if old_line_anchor:
568 %if old_line_anchor:
563 id="${old_line_anchor}"
569 id="${old_line_anchor}"
564 %endif
570 %endif
565 >
571 >
566 %if line.original.lineno:
572 %if line.original.lineno:
567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 %endif
574 %endif
569 </td>
575 </td>
570 <td class="cb-content ${action_class(line.original.action)}"
576 <td class="cb-content ${action_class(line.original.action)}"
571 data-line-no="o${line.original.lineno}"
577 data-line-no="o${line.original.lineno}"
572 >
578 >
573 %if use_comments and line.original.lineno:
579 %if use_comments and line.original.lineno:
574 ${render_add_comment_button()}
580 ${render_add_comment_button()}
575 %endif
581 %endif
576 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
577
583
578 %if use_comments and line.original.lineno and line_old_comments:
584 %if use_comments and line.original.lineno and line_old_comments:
579 ${inline_comments_container(line_old_comments, inline_comments)}
585 ${inline_comments_container(line_old_comments, inline_comments)}
580 %endif
586 %endif
581
587
582 </td>
588 </td>
583 <td class="cb-data ${action_class(line.modified.action)}"
589 <td class="cb-data ${action_class(line.modified.action)}"
584 data-line-no="${line.modified.lineno}"
590 data-line-no="${line.modified.lineno}"
585 >
591 >
586 <div>
592 <div>
587
593
588 %if line.modified.get_comment_args:
594 %if line.modified.get_comment_args:
589 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
590 %else:
596 %else:
591 <% line_new_comments = None%>
597 <% line_new_comments = None%>
592 %endif
598 %endif
593 %if line_new_comments:
599 %if line_new_comments:
594 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
595 % if has_outdated:
601 % if has_outdated:
596 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
597 % else:
603 % else:
598 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
599 % endif
605 % endif
600 %endif
606 %endif
601 </div>
607 </div>
602 </td>
608 </td>
603 <td class="cb-lineno ${action_class(line.modified.action)}"
609 <td class="cb-lineno ${action_class(line.modified.action)}"
604 data-line-no="${line.modified.lineno}"
610 data-line-no="${line.modified.lineno}"
605 %if new_line_anchor:
611 %if new_line_anchor:
606 id="${new_line_anchor}"
612 id="${new_line_anchor}"
607 %endif
613 %endif
608 >
614 >
609 %if line.modified.lineno:
615 %if line.modified.lineno:
610 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
611 %endif
617 %endif
612 </td>
618 </td>
613 <td class="cb-content ${action_class(line.modified.action)}"
619 <td class="cb-content ${action_class(line.modified.action)}"
614 data-line-no="n${line.modified.lineno}"
620 data-line-no="n${line.modified.lineno}"
615 >
621 >
616 %if use_comments and line.modified.lineno:
622 %if use_comments and line.modified.lineno:
617 ${render_add_comment_button()}
623 ${render_add_comment_button()}
618 %endif
624 %endif
619 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
625 <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:
626 %if use_comments and line.modified.lineno and line_new_comments:
621 ${inline_comments_container(line_new_comments, inline_comments)}
627 ${inline_comments_container(line_new_comments, inline_comments)}
622 %endif
628 %endif
623 </td>
629 </td>
624 </tr>
630 </tr>
625 %endfor
631 %endfor
626 </%def>
632 </%def>
627
633
628
634
629 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
635 <%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:
636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
631
637
632 <%
638 <%
633 old_line_anchor, new_line_anchor = None, None
639 old_line_anchor, new_line_anchor = None, None
634 if old_line_no:
640 if old_line_no:
635 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
636 if new_line_no:
642 if new_line_no:
637 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
638 %>
644 %>
639 <tr class="cb-line">
645 <tr class="cb-line">
640 <td class="cb-data ${action_class(action)}">
646 <td class="cb-data ${action_class(action)}">
641 <div>
647 <div>
642
648
643 %if comments_args:
649 %if comments_args:
644 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
645 %else:
651 %else:
646 <% comments = None %>
652 <% comments = None %>
647 %endif
653 %endif
648
654
649 % if comments:
655 % if comments:
650 <% has_outdated = any([x.outdated for x in comments]) %>
656 <% has_outdated = any([x.outdated for x in comments]) %>
651 % if has_outdated:
657 % if has_outdated:
652 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
653 % else:
659 % else:
654 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
655 % endif
661 % endif
656 % endif
662 % endif
657 </div>
663 </div>
658 </td>
664 </td>
659 <td class="cb-lineno ${action_class(action)}"
665 <td class="cb-lineno ${action_class(action)}"
660 data-line-no="${old_line_no}"
666 data-line-no="${old_line_no}"
661 %if old_line_anchor:
667 %if old_line_anchor:
662 id="${old_line_anchor}"
668 id="${old_line_anchor}"
663 %endif
669 %endif
664 >
670 >
665 %if old_line_anchor:
671 %if old_line_anchor:
666 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
667 %endif
673 %endif
668 </td>
674 </td>
669 <td class="cb-lineno ${action_class(action)}"
675 <td class="cb-lineno ${action_class(action)}"
670 data-line-no="${new_line_no}"
676 data-line-no="${new_line_no}"
671 %if new_line_anchor:
677 %if new_line_anchor:
672 id="${new_line_anchor}"
678 id="${new_line_anchor}"
673 %endif
679 %endif
674 >
680 >
675 %if new_line_anchor:
681 %if new_line_anchor:
676 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
677 %endif
683 %endif
678 </td>
684 </td>
679 <td class="cb-content ${action_class(action)}"
685 <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)}"
686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
681 >
687 >
682 %if use_comments:
688 %if use_comments:
683 ${render_add_comment_button()}
689 ${render_add_comment_button()}
684 %endif
690 %endif
685 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
686 %if use_comments and comments:
692 %if use_comments and comments:
687 ${inline_comments_container(comments, inline_comments)}
693 ${inline_comments_container(comments, inline_comments)}
688 %endif
694 %endif
689 </td>
695 </td>
690 </tr>
696 </tr>
691 %endfor
697 %endfor
692 </%def>
698 </%def>
693
699
694
700
695 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
696 % if diff_mode == 'unified':
702 % if diff_mode == 'unified':
697 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
698 % elif diff_mode == 'sideside':
704 % elif diff_mode == 'sideside':
699 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
700 % else:
706 % else:
701 <tr class="cb-line">
707 <tr class="cb-line">
702 <td>unknown diff mode</td>
708 <td>unknown diff mode</td>
703 </tr>
709 </tr>
704 % endif
710 % endif
705 </%def>file changes
711 </%def>file changes
706
712
707
713
708 <%def name="render_add_comment_button()">
714 <%def name="render_add_comment_button()">
709 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
715 <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>
716 <span><i class="icon-comment"></i></span>
711 </button>
717 </button>
712 </%def>
718 </%def>
713
719
714 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
715
721
716 <div id="diff-file-sticky" class="diffset-menu clearinner">
722 <div id="diff-file-sticky" class="diffset-menu clearinner">
717 ## auto adjustable
723 ## auto adjustable
718 <div class="sidebar__inner">
724 <div class="sidebar__inner">
719 <div class="sidebar__bar">
725 <div class="sidebar__bar">
720 <div class="pull-right">
726 <div class="pull-right">
721 <div class="btn-group">
727 <div class="btn-group">
722
728
723 ## DIFF OPTIONS via Select2
729 ## DIFF OPTIONS via Select2
724 <div class="pull-left">
730 <div class="pull-left">
725 ${h.hidden('diff_menu')}
731 ${h.hidden('diff_menu')}
726 </div>
732 </div>
727
733
728 <a
734 <a
729 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
730 title="${h.tooltip(_('View side by side'))}"
736 title="${h.tooltip(_('View side by side'))}"
731 href="${h.current_route_path(request, diffmode='sideside')}">
737 href="${h.current_route_path(request, diffmode='sideside')}">
732 <span>${_('Side by Side')}</span>
738 <span>${_('Side by Side')}</span>
733 </a>
739 </a>
734
740
735 <a
741 <a
736 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
742 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')}">
743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
738 <span>${_('Unified')}</span>
744 <span>${_('Unified')}</span>
739 </a>
745 </a>
740
746
741 % if range_diff_on is True:
747 % if range_diff_on is True:
742 <a
748 <a
743 title="${_('Turn off: Show the diff as commit range')}"
749 title="${_('Turn off: Show the diff as commit range')}"
744 class="btn btn-primary"
750 class="btn btn-primary"
745 href="${h.current_route_path(request, **{"range-diff":"0"})}">
751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
746 <span>${_('Range Diff')}</span>
752 <span>${_('Range Diff')}</span>
747 </a>
753 </a>
748 % elif range_diff_on is False:
754 % elif range_diff_on is False:
749 <a
755 <a
750 title="${_('Show the diff as commit range')}"
756 title="${_('Show the diff as commit range')}"
751 class="btn"
757 class="btn"
752 href="${h.current_route_path(request, **{"range-diff":"1"})}">
758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
753 <span>${_('Range Diff')}</span>
759 <span>${_('Range Diff')}</span>
754 </a>
760 </a>
755 % endif
761 % endif
756 </div>
762 </div>
757 </div>
763 </div>
758 <div class="pull-left">
764 <div class="pull-left">
759 <div class="btn-group">
765 <div class="btn-group">
760 <div class="pull-left">
766 <div class="pull-left">
761 ${h.hidden('file_filter')}
767 ${h.hidden('file_filter')}
762 </div>
768 </div>
763 <a
769 <a
764 class="btn"
770 class="btn"
765 href="#"
771 href="#"
766 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
767 <a
773 <a
768 class="btn"
774 class="btn"
769 href="#"
775 href="#"
770 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
771 </div>
777 </div>
772 </div>
778 </div>
773 </div>
779 </div>
774 <div class="fpath-placeholder">
780 <div class="fpath-placeholder">
775 <i class="icon-file-text"></i>
781 <i class="icon-file-text"></i>
776 <strong class="fpath-placeholder-text">
782 <strong class="fpath-placeholder-text">
777 Context file:
783 Context file:
778 </strong>
784 </strong>
779 </div>
785 </div>
780 <div class="sidebar_inner_shadow"></div>
786 <div class="sidebar_inner_shadow"></div>
781 </div>
787 </div>
782 </div>
788 </div>
783
789
784 % if diffset:
790 % if diffset:
785
791
786 %if diffset.limited_diff:
792 %if diffset.limited_diff:
787 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
788 %else:
794 %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}%>
795 <% 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
796 %endif
791 ## case on range-diff placeholder needs to be updated
797 ## case on range-diff placeholder needs to be updated
792 % if range_diff_on is True:
798 % if range_diff_on is True:
793 <% file_placeholder = _('Disabled on range diff') %>
799 <% file_placeholder = _('Disabled on range diff') %>
794 % endif
800 % endif
795
801
796 <script>
802 <script>
797
803
798 var feedFilesOptions = function (query, initialData) {
804 var feedFilesOptions = function (query, initialData) {
799 var data = {results: []};
805 var data = {results: []};
800 var isQuery = typeof query.term !== 'undefined';
806 var isQuery = typeof query.term !== 'undefined';
801
807
802 var section = _gettext('Changed files');
808 var section = _gettext('Changed files');
803 var filteredData = [];
809 var filteredData = [];
804
810
805 //filter results
811 //filter results
806 $.each(initialData.results, function (idx, value) {
812 $.each(initialData.results, function (idx, value) {
807
813
808 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
809 filteredData.push({
815 filteredData.push({
810 'id': this.id,
816 'id': this.id,
811 'text': this.text,
817 'text': this.text,
812 "ops": this.ops,
818 "ops": this.ops,
813 })
819 })
814 }
820 }
815
821
816 });
822 });
817
823
818 data.results = filteredData;
824 data.results = filteredData;
819
825
820 query.callback(data);
826 query.callback(data);
821 };
827 };
822
828
823 var formatFileResult = function(result, container, query, escapeMarkup) {
829 var formatFileResult = function(result, container, query, escapeMarkup) {
824 return function(data, escapeMarkup) {
830 return function(data, escapeMarkup) {
825 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
831 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']));
832 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">' +
833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
828 '<span class="pill" op="added">{0}</span>' +
834 '<span class="pill" op="added">{0}</span>' +
829 '<span class="pill" op="deleted">{1}</span>' +
835 '<span class="pill" op="deleted">{1}</span>' +
830 '</span>'
836 '</span>'
831 ;
837 ;
832 var added = data['ops']['added'];
838 var added = data['ops']['added'];
833 if (added === 0) {
839 if (added === 0) {
834 // don't show +0
840 // don't show +0
835 added = 0;
841 added = 0;
836 } else {
842 } else {
837 added = '+' + added;
843 added = '+' + added;
838 }
844 }
839
845
840 var deleted = -1*data['ops']['deleted'];
846 var deleted = -1*data['ops']['deleted'];
841
847
842 tmpl += pill.format(added, deleted);
848 tmpl += pill.format(added, deleted);
843 return container.format(tmpl);
849 return container.format(tmpl);
844
850
845 }(result, escapeMarkup);
851 }(result, escapeMarkup);
846 };
852 };
847 var preloadData = {
853 var preloadData = {
848 results: [
854 results: [
849 % for filediff in diffset.files:
855 % for filediff in diffset.files:
850 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
856 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
851 text:"${filediff.patch['filename']}",
857 text:"${filediff.patch['filename']}",
852 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
858 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
853 % endfor
859 % endfor
854 ]
860 ]
855 };
861 };
856
862
857 $(document).ready(function () {
863 $(document).ready(function () {
858
864
859 var fileFilter = $("#file_filter").select2({
865 var fileFilter = $("#file_filter").select2({
860 'dropdownAutoWidth': true,
866 'dropdownAutoWidth': true,
861 'width': 'auto',
867 'width': 'auto',
862 'placeholder': "${file_placeholder}",
868 'placeholder': "${file_placeholder}",
863 containerCssClass: "drop-menu",
869 containerCssClass: "drop-menu",
864 dropdownCssClass: "drop-menu-dropdown",
870 dropdownCssClass: "drop-menu-dropdown",
865 data: preloadData,
871 data: preloadData,
866 query: function(query) {
872 query: function(query) {
867 feedFilesOptions(query, preloadData);
873 feedFilesOptions(query, preloadData);
868 },
874 },
869 formatResult: formatFileResult
875 formatResult: formatFileResult
870 });
876 });
871 % if range_diff_on is True:
877 % if range_diff_on is True:
872 fileFilter.select2("enable", false);
878 fileFilter.select2("enable", false);
873
879
874 % endif
880 % endif
875
881
876 $("#file_filter").on('click', function (e) {
882 $("#file_filter").on('click', function (e) {
877 e.preventDefault();
883 e.preventDefault();
878 var selected = $('#file_filter').select2('data');
884 var selected = $('#file_filter').select2('data');
879 var idSelector = "#"+selected.id;
885 var idSelector = "#"+selected.id;
880 window.location.hash = idSelector;
886 window.location.hash = idSelector;
881 // expand the container if we quick-select the field
887 // expand the container if we quick-select the field
882 $(idSelector).next().prop('checked', false);
888 $(idSelector).next().prop('checked', false);
883 updateSticky()
889 updateSticky()
884 });
890 });
885
891
886 var contextPrefix = _gettext('Context file: ');
892 var contextPrefix = _gettext('Context file: ');
887 ## sticky sidebar
893 ## sticky sidebar
888 var sidebarElement = document.getElementById('diff-file-sticky');
894 var sidebarElement = document.getElementById('diff-file-sticky');
889 sidebar = new StickySidebar(sidebarElement, {
895 sidebar = new StickySidebar(sidebarElement, {
890 topSpacing: 0,
896 topSpacing: 0,
891 bottomSpacing: 0,
897 bottomSpacing: 0,
892 innerWrapperSelector: '.sidebar__inner'
898 innerWrapperSelector: '.sidebar__inner'
893 });
899 });
894 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
900 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
895 // reset our file so it's not holding new value
901 // reset our file so it's not holding new value
896 $('.fpath-placeholder-text').html(contextPrefix)
902 $('.fpath-placeholder-text').html(contextPrefix)
897 });
903 });
898
904
899 updateSticky = function () {
905 updateSticky = function () {
900 sidebar.updateSticky();
906 sidebar.updateSticky();
901 Waypoint.refreshAll();
907 Waypoint.refreshAll();
902 };
908 };
903
909
904 var animateText = $.debounce(100, function(fPath, anchorId) {
910 var animateText = $.debounce(100, function(fPath, anchorId) {
905 // animate setting the text
911 // animate setting the text
906 var callback = function () {
912 var callback = function () {
907 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
913 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
908 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
914 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
909 };
915 };
910 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
916 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
911 });
917 });
912
918
913 ## dynamic file waypoints
919 ## dynamic file waypoints
914 var setFPathInfo = function(fPath, anchorId){
920 var setFPathInfo = function(fPath, anchorId){
915 animateText(fPath, anchorId)
921 animateText(fPath, anchorId)
916 };
922 };
917
923
918 var codeBlock = $('.filediff');
924 var codeBlock = $('.filediff');
919 // forward waypoint
925 // forward waypoint
920 codeBlock.waypoint(
926 codeBlock.waypoint(
921 function(direction) {
927 function(direction) {
922 if (direction === "down"){
928 if (direction === "down"){
923 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
929 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
924 }
930 }
925 }, {
931 }, {
926 offset: 70,
932 offset: 70,
927 context: '.fpath-placeholder'
933 context: '.fpath-placeholder'
928 }
934 }
929 );
935 );
930
936
931 // backward waypoint
937 // backward waypoint
932 codeBlock.waypoint(
938 codeBlock.waypoint(
933 function(direction) {
939 function(direction) {
934 if (direction === "up"){
940 if (direction === "up"){
935 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
941 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
936 }
942 }
937 }, {
943 }, {
938 offset: function () {
944 offset: function () {
939 return -this.element.clientHeight + 90
945 return -this.element.clientHeight + 90
940 },
946 },
941 context: '.fpath-placeholder'
947 context: '.fpath-placeholder'
942 }
948 }
943 );
949 );
944
950
945 var preloadData = {
951 var preloadData = {
946 results: [
952 results: [
947 ## Wide diff mode
953 ## Wide diff mode
948 {
954 {
949 id: 1,
955 id: 1,
950 text: _gettext('Toggle Wide Mode diff'),
956 text: _gettext('Toggle Wide Mode diff'),
951 action: function () {
957 action: function () {
952 updateSticky();
958 updateSticky();
953 Rhodecode.comments.toggleWideMode(this);
959 Rhodecode.comments.toggleWideMode(this);
954 return null;
960 return null;
955 },
961 },
956 url: null,
962 url: null,
957 },
963 },
958
964
959 ## Whitespace change
965 ## Whitespace change
960 % if request.GET.get('ignorews', '') == '1':
966 % if request.GET.get('ignorews', '') == '1':
961 {
967 {
962 id: 2,
968 id: 2,
963 text: _gettext('Show whitespace changes'),
969 text: _gettext('Show whitespace changes'),
964 action: function () {},
970 action: function () {},
965 url: "${h.current_route_path(request, ignorews=0)|n}"
971 url: "${h.current_route_path(request, ignorews=0)|n}"
966 },
972 },
967 % else:
973 % else:
968 {
974 {
969 id: 2,
975 id: 2,
970 text: _gettext('Hide whitespace changes'),
976 text: _gettext('Hide whitespace changes'),
971 action: function () {},
977 action: function () {},
972 url: "${h.current_route_path(request, ignorews=1)|n}"
978 url: "${h.current_route_path(request, ignorews=1)|n}"
973 },
979 },
974 % endif
980 % endif
975
981
976 ## FULL CONTEXT
982 ## FULL CONTEXT
977 % if request.GET.get('fullcontext', '') == '1':
983 % if request.GET.get('fullcontext', '') == '1':
978 {
984 {
979 id: 3,
985 id: 3,
980 text: _gettext('Hide full context diff'),
986 text: _gettext('Hide full context diff'),
981 action: function () {},
987 action: function () {},
982 url: "${h.current_route_path(request, fullcontext=0)|n}"
988 url: "${h.current_route_path(request, fullcontext=0)|n}"
983 },
989 },
984 % else:
990 % else:
985 {
991 {
986 id: 3,
992 id: 3,
987 text: _gettext('Show full context diff'),
993 text: _gettext('Show full context diff'),
988 action: function () {},
994 action: function () {},
989 url: "${h.current_route_path(request, fullcontext=1)|n}"
995 url: "${h.current_route_path(request, fullcontext=1)|n}"
990 },
996 },
991 % endif
997 % endif
992
998
993 ]
999 ]
994 };
1000 };
995
1001
996 $("#diff_menu").select2({
1002 $("#diff_menu").select2({
997 minimumResultsForSearch: -1,
1003 minimumResultsForSearch: -1,
998 containerCssClass: "drop-menu",
1004 containerCssClass: "drop-menu",
999 dropdownCssClass: "drop-menu-dropdown",
1005 dropdownCssClass: "drop-menu-dropdown",
1000 dropdownAutoWidth: true,
1006 dropdownAutoWidth: true,
1001 data: preloadData,
1007 data: preloadData,
1002 placeholder: "${_('Diff Options')}",
1008 placeholder: "${_('Diff Options')}",
1003 });
1009 });
1004 $("#diff_menu").on('select2-selecting', function (e) {
1010 $("#diff_menu").on('select2-selecting', function (e) {
1005 e.choice.action();
1011 e.choice.action();
1006 if (e.choice.url !== null) {
1012 if (e.choice.url !== null) {
1007 window.location = e.choice.url
1013 window.location = e.choice.url
1008 }
1014 }
1009 });
1015 });
1010
1016
1011 });
1017 });
1012
1018
1013 </script>
1019 </script>
1014 % endif
1020 % endif
1015
1021
1016 </%def>
1022 </%def>
General Comments 0
You need to be logged in to leave comments. Login now