##// END OF EJS Templates
Fixed issue with inproper handling of diff parsing that could lead to infinit loops....
marcink -
r3022:0ed42ca7 beta
parent child Browse files
Show More
@@ -0,0 +1,416 b''
1 diff --git a/vcs/backends/base.py b/vcs/backends/base.py
2 index 212267ca23949807b8d89fa8ca495827dcfab3b1..ad17f16634da602503ed4ddd7cdd2e1ccdf4bed4 100644
3 --- a/vcs/backends/base.py
4 +++ b/vcs/backends/base.py
5 @@ -54,6 +54,7 @@ class BaseRepository(object):
6 """
7 scm = None
8 DEFAULT_BRANCH_NAME = None
9 + EMPTY_CHANGESET = '0' * 40
10
11 def __init__(self, repo_path, create=False, **kwargs):
12 """
13 @@ -204,6 +205,23 @@ class BaseRepository(object):
14 """
15 raise NotImplementedError
16
17 + def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
18 + context=3):
19 + """
20 + Returns (git like) *diff*, as plain text. Shows changes introduced by
21 + ``rev2`` since ``rev1``.
22 +
23 + :param rev1: Entry point from which diff is shown. Can be
24 + ``self.EMPTY_CHANGESET`` - in this case, patch showing all
25 + the changes since empty state of the repository until ``rev2``
26 + :param rev2: Until which revision changes should be shown.
27 + :param ignore_whitespace: If set to ``True``, would not show whitespace
28 + changes. Defaults to ``False``.
29 + :param context: How many lines before/after changed lines should be
30 + shown. Defaults to ``3``.
31 + """
32 + raise NotImplementedError
33 +
34 # ========== #
35 # COMMIT API #
36 # ========== #
37 @@ -341,7 +359,6 @@ class BaseChangeset(object):
38 otherwise; trying to access this attribute while there is no
39 changesets would raise ``EmptyRepositoryError``
40 """
41 -
42 def __str__(self):
43 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
44 self.short_id)
45 @@ -591,7 +608,6 @@ class BaseChangeset(object):
46 return data
47
48
49 -
50 class BaseWorkdir(object):
51 """
52 Working directory representation of single repository.
53 diff --git a/vcs/backends/git/repository.py b/vcs/backends/git/repository.py
54 index 8b9d1247fdee44e7a021b80e4965d8609cfd5720..e9f04e74dedd2f57417eb91dd2f4f7c61ec7e097 100644
55 --- a/vcs/backends/git/repository.py
56 +++ b/vcs/backends/git/repository.py
57 @@ -12,6 +12,7 @@
58 import os
59 import re
60 import time
61 +import inspect
62 import posixpath
63 from dulwich.repo import Repo, NotGitRepository
64 #from dulwich.config import ConfigFile
65 @@ -101,21 +102,6 @@ class GitRepository(BaseRepository):
66 "stderr:\n%s" % (cmd, se))
67 return so, se
68
69 - def _get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
70 - context=3):
71 - rev1 = self._get_revision(rev1)
72 - rev2 = self._get_revision(rev2)
73 -
74 - if ignore_whitespace:
75 - cmd = 'diff -U%s -w %s %s' % (context, rev1, rev2)
76 - else:
77 - cmd = 'diff -U%s %s %s' % (context, rev1, rev2)
78 - if path:
79 - cmd += ' -- "%s"' % path
80 - so, se = self.run_git_command(cmd)
81 -
82 - return so
83 -
84 def _check_url(self, url):
85 """
86 Functon will check given url and try to verify if it's a valid
87 @@ -322,6 +308,8 @@ class GitRepository(BaseRepository):
88 Returns ``GitChangeset`` object representing commit from git repository
89 at the given revision or head (most recent commit) if None given.
90 """
91 + if isinstance(revision, GitChangeset):
92 + return revision
93 revision = self._get_revision(revision)
94 changeset = GitChangeset(repository=self, revision=revision)
95 return changeset
96 @@ -398,6 +386,49 @@ class GitRepository(BaseRepository):
97 for rev in revs:
98 yield self.get_changeset(rev)
99
100 + def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
101 + context=3):
102 + """
103 + Returns (git like) *diff*, as plain text. Shows changes introduced by
104 + ``rev2`` since ``rev1``.
105 +
106 + :param rev1: Entry point from which diff is shown. Can be
107 + ``self.EMPTY_CHANGESET`` - in this case, patch showing all
108 + the changes since empty state of the repository until ``rev2``
109 + :param rev2: Until which revision changes should be shown.
110 + :param ignore_whitespace: If set to ``True``, would not show whitespace
111 + changes. Defaults to ``False``.
112 + :param context: How many lines before/after changed lines should be
113 + shown. Defaults to ``3``.
114 + """
115 + flags = ['-U%s' % context]
116 + if ignore_whitespace:
117 + flags.append('-w')
118 +
119 + if rev1 == self.EMPTY_CHANGESET:
120 + rev2 = self.get_changeset(rev2).raw_id
121 + cmd = ' '.join(['show'] + flags + [rev2])
122 + else:
123 + rev1 = self.get_changeset(rev1).raw_id
124 + rev2 = self.get_changeset(rev2).raw_id
125 + cmd = ' '.join(['diff'] + flags + [rev1, rev2])
126 +
127 + if path:
128 + cmd += ' -- "%s"' % path
129 + stdout, stderr = self.run_git_command(cmd)
130 + # If we used 'show' command, strip first few lines (until actual diff
131 + # starts)
132 + if rev1 == self.EMPTY_CHANGESET:
133 + lines = stdout.splitlines()
134 + x = 0
135 + for line in lines:
136 + if line.startswith('diff'):
137 + break
138 + x += 1
139 + # Append new line just like 'diff' command do
140 + stdout = '\n'.join(lines[x:]) + '\n'
141 + return stdout
142 +
143 @LazyProperty
144 def in_memory_changeset(self):
145 """
146 diff --git a/vcs/backends/hg.py b/vcs/backends/hg.py
147 index f1f9f95e4d476ab01d8e7b02a8b59034c0740a3b..b7d63c552c39b2f8aaec17ef46551369c8b8e793 100644
148 --- a/vcs/backends/hg.py
149 +++ b/vcs/backends/hg.py
150 @@ -256,13 +256,32 @@ class MercurialRepository(BaseRepository):
151
152 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
153
154 - def _get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
155 + def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
156 context=3):
157 + """
158 + Returns (git like) *diff*, as plain text. Shows changes introduced by
159 + ``rev2`` since ``rev1``.
160 +
161 + :param rev1: Entry point from which diff is shown. Can be
162 + ``self.EMPTY_CHANGESET`` - in this case, patch showing all
163 + the changes since empty state of the repository until ``rev2``
164 + :param rev2: Until which revision changes should be shown.
165 + :param ignore_whitespace: If set to ``True``, would not show whitespace
166 + changes. Defaults to ``False``.
167 + :param context: How many lines before/after changed lines should be
168 + shown. Defaults to ``3``.
169 + """
170 + # Check if given revisions are present at repository (may raise
171 + # ChangesetDoesNotExistError)
172 + if rev1 != self.EMPTY_CHANGESET:
173 + self.get_changeset(rev1)
174 + self.get_changeset(rev2)
175 +
176 file_filter = match(self.path, '', [path])
177 - return patch.diff(self._repo, rev1, rev2, match=file_filter,
178 + return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
179 opts=diffopts(git=True,
180 ignorews=ignore_whitespace,
181 - context=context))
182 + context=context)))
183
184 def _check_url(self, url):
185 """
186 diff --git a/vcs/tests/test_git.py b/vcs/tests/test_git.py
187 index 30da035a2a35c3dca14064778e97188b6d4ce5d6..d4b82b9e612af8bb5bf490a827377c7c2567735a 100644
188 --- a/vcs/tests/test_git.py
189 +++ b/vcs/tests/test_git.py
190 @@ -639,19 +639,19 @@ class GitSpecificWithRepoTest(BackendTestMixin, unittest.TestCase):
191
192 def test_get_diff_runs_git_command_with_hashes(self):
193 self.repo.run_git_command = mock.Mock(return_value=['', ''])
194 - self.repo._get_diff(0, 1)
195 + self.repo.get_diff(0, 1)
196 self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s' %
197 (3, self.repo._get_revision(0), self.repo._get_revision(1)))
198
199 def test_get_diff_runs_git_command_with_str_hashes(self):
200 self.repo.run_git_command = mock.Mock(return_value=['', ''])
201 - self.repo._get_diff('0' * 40, 1)
202 - self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s' %
203 - (3, self.repo._get_revision(0), self.repo._get_revision(1)))
204 + self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
205 + self.repo.run_git_command.assert_called_once_with('show -U%s %s' %
206 + (3, self.repo._get_revision(1)))
207
208 def test_get_diff_runs_git_command_with_path_if_its_given(self):
209 self.repo.run_git_command = mock.Mock(return_value=['', ''])
210 - self.repo._get_diff(0, 1, 'foo')
211 + self.repo.get_diff(0, 1, 'foo')
212 self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s -- "foo"'
213 % (3, self.repo._get_revision(0), self.repo._get_revision(1)))
214
215 diff --git a/vcs/tests/test_repository.py b/vcs/tests/test_repository.py
216 index e34033e29fa9b3d3366b723beab129cee73869b9..b6e3f419778d6009229e9108824acaf83eea1784 100644
217 --- a/vcs/tests/test_repository.py
218 +++ b/vcs/tests/test_repository.py
219 @@ -1,9 +1,12 @@
220 from __future__ import with_statement
221 +import datetime
222 from base import BackendTestMixin
223 from conf import SCM_TESTS
224 +from conf import TEST_USER_CONFIG_FILE
225 +from vcs.nodes import FileNode
226 from vcs.utils.compat import unittest
227 +from vcs.exceptions import ChangesetDoesNotExistError
228
229 -from conf import TEST_USER_CONFIG_FILE
230
231 class RepositoryBaseTest(BackendTestMixin):
232 recreate_repo_per_test = False
233 @@ -29,6 +32,176 @@ class RepositoryBaseTest(BackendTestMixin):
234 'foo.bar@example.com')
235
236
237 +
238 +class RepositoryGetDiffTest(BackendTestMixin):
239 +
240 + @classmethod
241 + def _get_commits(cls):
242 + commits = [
243 + {
244 + 'message': 'Initial commit',
245 + 'author': 'Joe Doe <joe.doe@example.com>',
246 + 'date': datetime.datetime(2010, 1, 1, 20),
247 + 'added': [
248 + FileNode('foobar', content='foobar'),
249 + FileNode('foobar2', content='foobar2'),
250 + ],
251 + },
252 + {
253 + 'message': 'Changed foobar, added foobar3',
254 + 'author': 'Jane Doe <jane.doe@example.com>',
255 + 'date': datetime.datetime(2010, 1, 1, 21),
256 + 'added': [
257 + FileNode('foobar3', content='foobar3'),
258 + ],
259 + 'changed': [
260 + FileNode('foobar', 'FOOBAR'),
261 + ],
262 + },
263 + {
264 + 'message': 'Removed foobar, changed foobar3',
265 + 'author': 'Jane Doe <jane.doe@example.com>',
266 + 'date': datetime.datetime(2010, 1, 1, 22),
267 + 'changed': [
268 + FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'),
269 + ],
270 + 'removed': [FileNode('foobar')],
271 + },
272 + ]
273 + return commits
274 +
275 + def test_raise_for_wrong(self):
276 + with self.assertRaises(ChangesetDoesNotExistError):
277 + self.repo.get_diff('a' * 40, 'b' * 40)
278 +
279 +class GitRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase):
280 + backend_alias = 'git'
281 +
282 + def test_initial_commit_diff(self):
283 + initial_rev = self.repo.revisions[0]
284 + self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar
285 +new file mode 100644
286 +index 0000000..f6ea049
287 +--- /dev/null
288 ++++ b/foobar
289 +@@ -0,0 +1 @@
290 ++foobar
291 +\ No newline at end of file
292 +diff --git a/foobar2 b/foobar2
293 +new file mode 100644
294 +index 0000000..e8c9d6b
295 +--- /dev/null
296 ++++ b/foobar2
297 +@@ -0,0 +1 @@
298 ++foobar2
299 +\ No newline at end of file
300 +''')
301 +
302 + def test_second_changeset_diff(self):
303 + revs = self.repo.revisions
304 + self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar
305 +index f6ea049..389865b 100644
306 +--- a/foobar
307 ++++ b/foobar
308 +@@ -1 +1 @@
309 +-foobar
310 +\ No newline at end of file
311 ++FOOBAR
312 +\ No newline at end of file
313 +diff --git a/foobar3 b/foobar3
314 +new file mode 100644
315 +index 0000000..c11c37d
316 +--- /dev/null
317 ++++ b/foobar3
318 +@@ -0,0 +1 @@
319 ++foobar3
320 +\ No newline at end of file
321 +''')
322 +
323 + def test_third_changeset_diff(self):
324 + revs = self.repo.revisions
325 + self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar
326 +deleted file mode 100644
327 +index 389865b..0000000
328 +--- a/foobar
329 ++++ /dev/null
330 +@@ -1 +0,0 @@
331 +-FOOBAR
332 +\ No newline at end of file
333 +diff --git a/foobar3 b/foobar3
334 +index c11c37d..f932447 100644
335 +--- a/foobar3
336 ++++ b/foobar3
337 +@@ -1 +1,3 @@
338 +-foobar3
339 +\ No newline at end of file
340 ++FOOBAR
341 ++FOOBAR
342 ++FOOBAR
343 +''')
344 +
345 +
346 +class HgRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase):
347 + backend_alias = 'hg'
348 +
349 + def test_initial_commit_diff(self):
350 + initial_rev = self.repo.revisions[0]
351 + self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar
352 +new file mode 100755
353 +--- /dev/null
354 ++++ b/foobar
355 +@@ -0,0 +1,1 @@
356 ++foobar
357 +\ No newline at end of file
358 +diff --git a/foobar2 b/foobar2
359 +new file mode 100755
360 +--- /dev/null
361 ++++ b/foobar2
362 +@@ -0,0 +1,1 @@
363 ++foobar2
364 +\ No newline at end of file
365 +''')
366 +
367 + def test_second_changeset_diff(self):
368 + revs = self.repo.revisions
369 + self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar
370 +--- a/foobar
371 ++++ b/foobar
372 +@@ -1,1 +1,1 @@
373 +-foobar
374 +\ No newline at end of file
375 ++FOOBAR
376 +\ No newline at end of file
377 +diff --git a/foobar3 b/foobar3
378 +new file mode 100755
379 +--- /dev/null
380 ++++ b/foobar3
381 +@@ -0,0 +1,1 @@
382 ++foobar3
383 +\ No newline at end of file
384 +''')
385 +
386 + def test_third_changeset_diff(self):
387 + revs = self.repo.revisions
388 + self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar
389 +deleted file mode 100755
390 +--- a/foobar
391 ++++ /dev/null
392 +@@ -1,1 +0,0 @@
393 +-FOOBAR
394 +\ No newline at end of file
395 +diff --git a/foobar3 b/foobar3
396 +--- a/foobar3
397 ++++ b/foobar3
398 +@@ -1,1 +1,3 @@
399 +-foobar3
400 +\ No newline at end of file
401 ++FOOBAR
402 ++FOOBAR
403 ++FOOBAR
404 +''')
405 +
406 +
407 # For each backend create test case class
408 for alias in SCM_TESTS:
409 attrs = {
410 @@ -38,7 +211,6 @@ for alias in SCM_TESTS:
411 bases = (RepositoryBaseTest, unittest.TestCase)
412 globals()[cls_name] = type(cls_name, bases, attrs)
413
414 -
415 if __name__ == '__main__':
416 unittest.main()
@@ -1,768 +1,769 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import difflib
29 import difflib
30 import logging
30 import logging
31 import traceback
31 import traceback
32
32
33 from itertools import tee, imap
33 from itertools import tee, imap
34
34
35 from mercurial import patch
35 from mercurial import patch
36 from mercurial.mdiff import diffopts
36 from mercurial.mdiff import diffopts
37 from mercurial.bundlerepo import bundlerepository
37 from mercurial.bundlerepo import bundlerepository
38
38
39 from pylons.i18n.translation import _
39 from pylons.i18n.translation import _
40
40
41 from rhodecode.lib.compat import BytesIO
41 from rhodecode.lib.compat import BytesIO
42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
43 from rhodecode.lib.vcs.exceptions import VCSError
43 from rhodecode.lib.vcs.exceptions import VCSError
44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 from rhodecode.lib.helpers import escape
46 from rhodecode.lib.helpers import escape
47 from rhodecode.lib.utils import make_ui
47 from rhodecode.lib.utils import make_ui
48 from rhodecode.lib.utils2 import safe_unicode
48 from rhodecode.lib.utils2 import safe_unicode
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 def wrap_to_table(str_):
53 def wrap_to_table(str_):
54 return '''<table class="code-difftable">
54 return '''<table class="code-difftable">
55 <tr class="line no-comment">
55 <tr class="line no-comment">
56 <td class="lineno new"></td>
56 <td class="lineno new"></td>
57 <td class="code no-comment"><pre>%s</pre></td>
57 <td class="code no-comment"><pre>%s</pre></td>
58 </tr>
58 </tr>
59 </table>''' % str_
59 </table>''' % str_
60
60
61
61
62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
63 ignore_whitespace=True, line_context=3,
63 ignore_whitespace=True, line_context=3,
64 enable_comments=False):
64 enable_comments=False):
65 """
65 """
66 returns a wrapped diff into a table, checks for cut_off_limit and presents
66 returns a wrapped diff into a table, checks for cut_off_limit and presents
67 proper message
67 proper message
68 """
68 """
69
69
70 if filenode_old is None:
70 if filenode_old is None:
71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
72
72
73 if filenode_old.is_binary or filenode_new.is_binary:
73 if filenode_old.is_binary or filenode_new.is_binary:
74 diff = wrap_to_table(_('binary file'))
74 diff = wrap_to_table(_('binary file'))
75 stats = (0, 0)
75 stats = (0, 0)
76 size = 0
76 size = 0
77
77
78 elif cut_off_limit != -1 and (cut_off_limit is None or
78 elif cut_off_limit != -1 and (cut_off_limit is None or
79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
80
80
81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
82 ignore_whitespace=ignore_whitespace,
82 ignore_whitespace=ignore_whitespace,
83 context=line_context)
83 context=line_context)
84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
85
85
86 diff = diff_processor.as_html(enable_comments=enable_comments)
86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 stats = diff_processor.stat()
87 stats = diff_processor.stat()
88 size = len(diff or '')
88 size = len(diff or '')
89 else:
89 else:
90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
91 'diff menu to display this diff'))
91 'diff menu to display this diff'))
92 stats = (0, 0)
92 stats = (0, 0)
93 size = 0
93 size = 0
94 if not diff:
94 if not diff:
95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
96 [filenode_new, filenode_old])
96 [filenode_new, filenode_old])
97 if submodules:
97 if submodules:
98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
99 else:
99 else:
100 diff = wrap_to_table(_('No changes detected'))
100 diff = wrap_to_table(_('No changes detected'))
101
101
102 cs1 = filenode_old.changeset.raw_id
102 cs1 = filenode_old.changeset.raw_id
103 cs2 = filenode_new.changeset.raw_id
103 cs2 = filenode_new.changeset.raw_id
104
104
105 return size, cs1, cs2, diff, stats
105 return size, cs1, cs2, diff, stats
106
106
107
107
108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
109 """
109 """
110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
111
111
112 :param ignore_whitespace: ignore whitespaces in diff
112 :param ignore_whitespace: ignore whitespaces in diff
113 """
113 """
114 # make sure we pass in default context
114 # make sure we pass in default context
115 context = context or 3
115 context = context or 3
116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
117 [filenode_new, filenode_old])
117 [filenode_new, filenode_old])
118 if submodules:
118 if submodules:
119 return ''
119 return ''
120
120
121 for filenode in (filenode_old, filenode_new):
121 for filenode in (filenode_old, filenode_new):
122 if not isinstance(filenode, FileNode):
122 if not isinstance(filenode, FileNode):
123 raise VCSError("Given object should be FileNode object, not %s"
123 raise VCSError("Given object should be FileNode object, not %s"
124 % filenode.__class__)
124 % filenode.__class__)
125
125
126 repo = filenode_new.changeset.repository
126 repo = filenode_new.changeset.repository
127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
129
129
130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
131 ignore_whitespace, context)
131 ignore_whitespace, context)
132 return vcs_gitdiff
132 return vcs_gitdiff
133
133
134 NEW_FILENODE = 1
134 NEW_FILENODE = 1
135 DEL_FILENODE = 2
135 DEL_FILENODE = 2
136 MOD_FILENODE = 3
136 MOD_FILENODE = 3
137 RENAMED_FILENODE = 4
137 RENAMED_FILENODE = 4
138 CHMOD_FILENODE = 5
138 CHMOD_FILENODE = 5
139
139
140
140
141 class DiffLimitExceeded(Exception):
141 class DiffLimitExceeded(Exception):
142 pass
142 pass
143
143
144
144
145 class LimitedDiffContainer(object):
145 class LimitedDiffContainer(object):
146
146
147 def __init__(self, diff_limit, cur_diff_size, diff):
147 def __init__(self, diff_limit, cur_diff_size, diff):
148 self.diff = diff
148 self.diff = diff
149 self.diff_limit = diff_limit
149 self.diff_limit = diff_limit
150 self.cur_diff_size = cur_diff_size
150 self.cur_diff_size = cur_diff_size
151
151
152 def __iter__(self):
152 def __iter__(self):
153 for l in self.diff:
153 for l in self.diff:
154 yield l
154 yield l
155
155
156
156
157 class DiffProcessor(object):
157 class DiffProcessor(object):
158 """
158 """
159 Give it a unified or git diff and it returns a list of the files that were
159 Give it a unified or git diff and it returns a list of the files that were
160 mentioned in the diff together with a dict of meta information that
160 mentioned in the diff together with a dict of meta information that
161 can be used to render it in a HTML template.
161 can be used to render it in a HTML template.
162 """
162 """
163 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
163 _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
164 _newline_marker = '\\ No newline at end of file\n'
164 _newline_marker = re.compile(r'^\\ No newline at end of file')
165 _git_header_re = re.compile(r"""
165 _git_header_re = re.compile(r"""
166 #^diff[ ]--git
166 #^diff[ ]--git
167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
179 """, re.VERBOSE | re.MULTILINE)
179 """, re.VERBOSE | re.MULTILINE)
180 _hg_header_re = re.compile(r"""
180 _hg_header_re = re.compile(r"""
181 #^diff[ ]--git
181 #^diff[ ]--git
182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
194 """, re.VERBOSE | re.MULTILINE)
194 """, re.VERBOSE | re.MULTILINE)
195
195
196 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
196 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
197 """
197 """
198 :param diff: a text in diff format
198 :param diff: a text in diff format
199 :param vcs: type of version controll hg or git
199 :param vcs: type of version controll hg or git
200 :param format: format of diff passed, `udiff` or `gitdiff`
200 :param format: format of diff passed, `udiff` or `gitdiff`
201 :param diff_limit: define the size of diff that is considered "big"
201 :param diff_limit: define the size of diff that is considered "big"
202 based on that parameter cut off will be triggered, set to None
202 based on that parameter cut off will be triggered, set to None
203 to show full diff
203 to show full diff
204 """
204 """
205 if not isinstance(diff, basestring):
205 if not isinstance(diff, basestring):
206 raise Exception('Diff must be a basestring got %s instead' % type(diff))
206 raise Exception('Diff must be a basestring got %s instead' % type(diff))
207
207
208 self._diff = diff
208 self._diff = diff
209 self._format = format
209 self._format = format
210 self.adds = 0
210 self.adds = 0
211 self.removes = 0
211 self.removes = 0
212 # calculate diff size
212 # calculate diff size
213 self.diff_size = len(diff)
213 self.diff_size = len(diff)
214 self.diff_limit = diff_limit
214 self.diff_limit = diff_limit
215 self.cur_diff_size = 0
215 self.cur_diff_size = 0
216 self.parsed = False
216 self.parsed = False
217 self.parsed_diff = []
217 self.parsed_diff = []
218 self.vcs = vcs
218 self.vcs = vcs
219
219
220 if format == 'gitdiff':
220 if format == 'gitdiff':
221 self.differ = self._highlight_line_difflib
221 self.differ = self._highlight_line_difflib
222 self._parser = self._parse_gitdiff
222 self._parser = self._parse_gitdiff
223 else:
223 else:
224 self.differ = self._highlight_line_udiff
224 self.differ = self._highlight_line_udiff
225 self._parser = self._parse_udiff
225 self._parser = self._parse_udiff
226
226
227 def _copy_iterator(self):
227 def _copy_iterator(self):
228 """
228 """
229 make a fresh copy of generator, we should not iterate thru
229 make a fresh copy of generator, we should not iterate thru
230 an original as it's needed for repeating operations on
230 an original as it's needed for repeating operations on
231 this instance of DiffProcessor
231 this instance of DiffProcessor
232 """
232 """
233 self.__udiff, iterator_copy = tee(self.__udiff)
233 self.__udiff, iterator_copy = tee(self.__udiff)
234 return iterator_copy
234 return iterator_copy
235
235
236 def _escaper(self, string):
236 def _escaper(self, string):
237 """
237 """
238 Escaper for diff escapes special chars and checks the diff limit
238 Escaper for diff escapes special chars and checks the diff limit
239
239
240 :param string:
240 :param string:
241 :type string:
241 :type string:
242 """
242 """
243
243
244 self.cur_diff_size += len(string)
244 self.cur_diff_size += len(string)
245
245
246 # escaper get's iterated on each .next() call and it checks if each
246 # escaper get's iterated on each .next() call and it checks if each
247 # parsed line doesn't exceed the diff limit
247 # parsed line doesn't exceed the diff limit
248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
249 raise DiffLimitExceeded('Diff Limit Exceeded')
249 raise DiffLimitExceeded('Diff Limit Exceeded')
250
250
251 return safe_unicode(string).replace('&', '&amp;')\
251 return safe_unicode(string).replace('&', '&amp;')\
252 .replace('<', '&lt;')\
252 .replace('<', '&lt;')\
253 .replace('>', '&gt;')
253 .replace('>', '&gt;')
254
254
255 def _line_counter(self, l):
255 def _line_counter(self, l):
256 """
256 """
257 Checks each line and bumps total adds/removes for this diff
257 Checks each line and bumps total adds/removes for this diff
258
258
259 :param l:
259 :param l:
260 """
260 """
261 if l.startswith('+') and not l.startswith('+++'):
261 if l.startswith('+') and not l.startswith('+++'):
262 self.adds += 1
262 self.adds += 1
263 elif l.startswith('-') and not l.startswith('---'):
263 elif l.startswith('-') and not l.startswith('---'):
264 self.removes += 1
264 self.removes += 1
265 return safe_unicode(l)
265 return safe_unicode(l)
266
266
267 def _highlight_line_difflib(self, line, next_):
267 def _highlight_line_difflib(self, line, next_):
268 """
268 """
269 Highlight inline changes in both lines.
269 Highlight inline changes in both lines.
270 """
270 """
271
271
272 if line['action'] == 'del':
272 if line['action'] == 'del':
273 old, new = line, next_
273 old, new = line, next_
274 else:
274 else:
275 old, new = next_, line
275 old, new = next_, line
276
276
277 oldwords = re.split(r'(\W)', old['line'])
277 oldwords = re.split(r'(\W)', old['line'])
278 newwords = re.split(r'(\W)', new['line'])
278 newwords = re.split(r'(\W)', new['line'])
279
279
280 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
280 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
281
281
282 oldfragments, newfragments = [], []
282 oldfragments, newfragments = [], []
283 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
283 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
284 oldfrag = ''.join(oldwords[i1:i2])
284 oldfrag = ''.join(oldwords[i1:i2])
285 newfrag = ''.join(newwords[j1:j2])
285 newfrag = ''.join(newwords[j1:j2])
286 if tag != 'equal':
286 if tag != 'equal':
287 if oldfrag:
287 if oldfrag:
288 oldfrag = '<del>%s</del>' % oldfrag
288 oldfrag = '<del>%s</del>' % oldfrag
289 if newfrag:
289 if newfrag:
290 newfrag = '<ins>%s</ins>' % newfrag
290 newfrag = '<ins>%s</ins>' % newfrag
291 oldfragments.append(oldfrag)
291 oldfragments.append(oldfrag)
292 newfragments.append(newfrag)
292 newfragments.append(newfrag)
293
293
294 old['line'] = "".join(oldfragments)
294 old['line'] = "".join(oldfragments)
295 new['line'] = "".join(newfragments)
295 new['line'] = "".join(newfragments)
296
296
297 def _highlight_line_udiff(self, line, next_):
297 def _highlight_line_udiff(self, line, next_):
298 """
298 """
299 Highlight inline changes in both lines.
299 Highlight inline changes in both lines.
300 """
300 """
301 start = 0
301 start = 0
302 limit = min(len(line['line']), len(next_['line']))
302 limit = min(len(line['line']), len(next_['line']))
303 while start < limit and line['line'][start] == next_['line'][start]:
303 while start < limit and line['line'][start] == next_['line'][start]:
304 start += 1
304 start += 1
305 end = -1
305 end = -1
306 limit -= start
306 limit -= start
307 while -end <= limit and line['line'][end] == next_['line'][end]:
307 while -end <= limit and line['line'][end] == next_['line'][end]:
308 end -= 1
308 end -= 1
309 end += 1
309 end += 1
310 if start or end:
310 if start or end:
311 def do(l):
311 def do(l):
312 last = end + len(l['line'])
312 last = end + len(l['line'])
313 if l['action'] == 'add':
313 if l['action'] == 'add':
314 tag = 'ins'
314 tag = 'ins'
315 else:
315 else:
316 tag = 'del'
316 tag = 'del'
317 l['line'] = '%s<%s>%s</%s>%s' % (
317 l['line'] = '%s<%s>%s</%s>%s' % (
318 l['line'][:start],
318 l['line'][:start],
319 tag,
319 tag,
320 l['line'][start:last],
320 l['line'][start:last],
321 tag,
321 tag,
322 l['line'][last:]
322 l['line'][last:]
323 )
323 )
324 do(line)
324 do(line)
325 do(next_)
325 do(next_)
326
326
327 def _get_header(self, diff_chunk):
327 def _get_header(self, diff_chunk):
328 """
328 """
329 parses the diff header, and returns parts, and leftover diff
329 parses the diff header, and returns parts, and leftover diff
330 parts consists of 14 elements::
330 parts consists of 14 elements::
331
331
332 a_path, b_path, similarity_index, rename_from, rename_to,
332 a_path, b_path, similarity_index, rename_from, rename_to,
333 old_mode, new_mode, new_file_mode, deleted_file_mode,
333 old_mode, new_mode, new_file_mode, deleted_file_mode,
334 a_blob_id, b_blob_id, b_mode, a_file, b_file
334 a_blob_id, b_blob_id, b_mode, a_file, b_file
335
335
336 :param diff_chunk:
336 :param diff_chunk:
337 :type diff_chunk:
337 :type diff_chunk:
338 """
338 """
339
339
340 if self.vcs == 'git':
340 if self.vcs == 'git':
341 match = self._git_header_re.match(diff_chunk)
341 match = self._git_header_re.match(diff_chunk)
342 diff = diff_chunk[match.end():]
342 diff = diff_chunk[match.end():]
343 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
343 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
344 elif self.vcs == 'hg':
344 elif self.vcs == 'hg':
345 match = self._hg_header_re.match(diff_chunk)
345 match = self._hg_header_re.match(diff_chunk)
346 diff = diff_chunk[match.end():]
346 diff = diff_chunk[match.end():]
347 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
347 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
348 else:
348 else:
349 raise Exception('VCS type %s is not supported' % self.vcs)
349 raise Exception('VCS type %s is not supported' % self.vcs)
350
350
351 def _clean_line(self, line, command):
352 if command in ['+', '-', ' ']:
353 #only modify the line if it's actually a diff thing
354 line = line[1:]
355 return line
356
351 def _parse_gitdiff(self, inline_diff=True):
357 def _parse_gitdiff(self, inline_diff=True):
352 _files = []
358 _files = []
353 diff_container = lambda arg: arg
359 diff_container = lambda arg: arg
354
360
355 ##split the diff in chunks of separate --git a/file b/file chunks
361 ##split the diff in chunks of separate --git a/file b/file chunks
356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
362 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 binary = False
363 binary = False
358 binary_msg = 'unknown binary'
364 binary_msg = 'unknown binary'
359 head, diff = self._get_header(raw_diff)
365 head, diff = self._get_header(raw_diff)
360
366
361 if not head['a_file'] and head['b_file']:
367 if not head['a_file'] and head['b_file']:
362 op = 'A'
368 op = 'A'
363 elif head['a_file'] and head['b_file']:
369 elif head['a_file'] and head['b_file']:
364 op = 'M'
370 op = 'M'
365 elif head['a_file'] and not head['b_file']:
371 elif head['a_file'] and not head['b_file']:
366 op = 'D'
372 op = 'D'
367 else:
373 else:
368 #probably we're dealing with a binary file 1
374 #probably we're dealing with a binary file 1
369 binary = True
375 binary = True
370 if head['deleted_file_mode']:
376 if head['deleted_file_mode']:
371 op = 'D'
377 op = 'D'
372 stats = ['b', DEL_FILENODE]
378 stats = ['b', DEL_FILENODE]
373 binary_msg = 'deleted binary file'
379 binary_msg = 'deleted binary file'
374 elif head['new_file_mode']:
380 elif head['new_file_mode']:
375 op = 'A'
381 op = 'A'
376 stats = ['b', NEW_FILENODE]
382 stats = ['b', NEW_FILENODE]
377 binary_msg = 'new binary file %s' % head['new_file_mode']
383 binary_msg = 'new binary file %s' % head['new_file_mode']
378 else:
384 else:
379 if head['new_mode'] and head['old_mode']:
385 if head['new_mode'] and head['old_mode']:
380 stats = ['b', CHMOD_FILENODE]
386 stats = ['b', CHMOD_FILENODE]
381 op = 'M'
387 op = 'M'
382 binary_msg = ('modified binary file chmod %s => %s'
388 binary_msg = ('modified binary file chmod %s => %s'
383 % (head['old_mode'], head['new_mode']))
389 % (head['old_mode'], head['new_mode']))
384 elif (head['rename_from'] and head['rename_to']
390 elif (head['rename_from'] and head['rename_to']
385 and head['rename_from'] != head['rename_to']):
391 and head['rename_from'] != head['rename_to']):
386 stats = ['b', RENAMED_FILENODE]
392 stats = ['b', RENAMED_FILENODE]
387 op = 'M'
393 op = 'M'
388 binary_msg = ('file renamed from %s to %s'
394 binary_msg = ('file renamed from %s to %s'
389 % (head['rename_from'], head['rename_to']))
395 % (head['rename_from'], head['rename_to']))
390 else:
396 else:
391 stats = ['b', MOD_FILENODE]
397 stats = ['b', MOD_FILENODE]
392 op = 'M'
398 op = 'M'
393 binary_msg = 'modified binary file'
399 binary_msg = 'modified binary file'
394
400
395 if not binary:
401 if not binary:
396 try:
402 try:
397 chunks, stats = self._parse_lines(diff)
403 chunks, stats = self._parse_lines(diff)
398 except DiffLimitExceeded:
404 except DiffLimitExceeded:
399 diff_container = lambda _diff: LimitedDiffContainer(
405 diff_container = lambda _diff: LimitedDiffContainer(
400 self.diff_limit,
406 self.diff_limit,
401 self.cur_diff_size,
407 self.cur_diff_size,
402 _diff)
408 _diff)
403 break
409 break
404 else:
410 else:
405 chunks = []
411 chunks = []
406 chunks.append([{
412 chunks.append([{
407 'old_lineno': '',
413 'old_lineno': '',
408 'new_lineno': '',
414 'new_lineno': '',
409 'action': 'binary',
415 'action': 'binary',
410 'line': binary_msg,
416 'line': binary_msg,
411 }])
417 }])
412
418
413 _files.append({
419 _files.append({
414 'filename': head['b_path'],
420 'filename': head['b_path'],
415 'old_revision': head['a_blob_id'],
421 'old_revision': head['a_blob_id'],
416 'new_revision': head['b_blob_id'],
422 'new_revision': head['b_blob_id'],
417 'chunks': chunks,
423 'chunks': chunks,
418 'operation': op,
424 'operation': op,
419 'stats': stats,
425 'stats': stats,
420 })
426 })
421
427
422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
428 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423
429
424 if inline_diff is False:
430 if inline_diff is False:
425 return diff_container(sorted(_files, key=sorter))
431 return diff_container(sorted(_files, key=sorter))
426
432
427 # highlight inline changes
433 # highlight inline changes
428 for diff_data in _files:
434 for diff_data in _files:
429 for chunk in diff_data['chunks']:
435 for chunk in diff_data['chunks']:
430 lineiter = iter(chunk)
436 lineiter = iter(chunk)
431 try:
437 try:
432 while 1:
438 while 1:
433 line = lineiter.next()
439 line = lineiter.next()
434 if line['action'] not in ['unmod', 'context']:
440 if line['action'] not in ['unmod', 'context']:
435 nextline = lineiter.next()
441 nextline = lineiter.next()
436 if nextline['action'] in ['unmod', 'context'] or \
442 if nextline['action'] in ['unmod', 'context'] or \
437 nextline['action'] == line['action']:
443 nextline['action'] == line['action']:
438 continue
444 continue
439 self.differ(line, nextline)
445 self.differ(line, nextline)
440 except StopIteration:
446 except StopIteration:
441 pass
447 pass
442
448
443 return diff_container(sorted(_files, key=sorter))
449 return diff_container(sorted(_files, key=sorter))
444
450
445 def _parse_udiff(self, inline_diff=True):
451 def _parse_udiff(self, inline_diff=True):
446 raise NotImplementedError()
452 raise NotImplementedError()
447
453
448 def _parse_lines(self, diff):
454 def _parse_lines(self, diff):
449 """
455 """
450 Parse the diff an return data for the template.
456 Parse the diff an return data for the template.
451 """
457 """
452
458
453 lineiter = iter(diff)
459 lineiter = iter(diff)
454 stats = [0, 0]
460 stats = [0, 0]
455
461
456 try:
462 try:
457 chunks = []
463 chunks = []
458 line = lineiter.next()
464 line = lineiter.next()
459
465
460 while line:
466 while line:
461 lines = []
467 lines = []
462 chunks.append(lines)
468 chunks.append(lines)
463
469
464 match = self._chunk_re.match(line)
470 match = self._chunk_re.match(line)
465
471
466 if not match:
472 if not match:
467 break
473 break
468
474
469 gr = match.groups()
475 gr = match.groups()
470 (old_line, old_end,
476 (old_line, old_end,
471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
477 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 old_line -= 1
478 old_line -= 1
473 new_line -= 1
479 new_line -= 1
474
480
475 context = len(gr) == 5
481 context = len(gr) == 5
476 old_end += old_line
482 old_end += old_line
477 new_end += new_line
483 new_end += new_line
478
484
479 if context:
485 if context:
480 # skip context only if it's first line
486 # skip context only if it's first line
481 if int(gr[0]) > 1:
487 if int(gr[0]) > 1:
482 lines.append({
488 lines.append({
483 'old_lineno': '...',
489 'old_lineno': '...',
484 'new_lineno': '...',
490 'new_lineno': '...',
485 'action': 'context',
491 'action': 'context',
486 'line': line,
492 'line': line,
487 })
493 })
488
494
489 line = lineiter.next()
495 line = lineiter.next()
490
496
491 while old_line < old_end or new_line < new_end:
497 while old_line < old_end or new_line < new_end:
498 command = ' '
492 if line:
499 if line:
493 command = line[0]
500 command = line[0]
494 if command in ['+', '-', ' ']:
495 #only modify the line if it's actually a diff
496 # thing
497 line = line[1:]
498 else:
499 command = ' '
500
501
501 affects_old = affects_new = False
502 affects_old = affects_new = False
502
503
503 # ignore those if we don't expect them
504 # ignore those if we don't expect them
504 if command in '#@':
505 if command in '#@':
505 continue
506 continue
506 elif command == '+':
507 elif command == '+':
507 affects_new = True
508 affects_new = True
508 action = 'add'
509 action = 'add'
509 stats[0] += 1
510 stats[0] += 1
510 elif command == '-':
511 elif command == '-':
511 affects_old = True
512 affects_old = True
512 action = 'del'
513 action = 'del'
513 stats[1] += 1
514 stats[1] += 1
514 else:
515 else:
515 affects_old = affects_new = True
516 affects_old = affects_new = True
516 action = 'unmod'
517 action = 'unmod'
517
518
518 if line != self._newline_marker:
519 if not self._newline_marker.match(line):
519 old_line += affects_old
520 old_line += affects_old
520 new_line += affects_new
521 new_line += affects_new
521 lines.append({
522 lines.append({
522 'old_lineno': affects_old and old_line or '',
523 'old_lineno': affects_old and old_line or '',
523 'new_lineno': affects_new and new_line or '',
524 'new_lineno': affects_new and new_line or '',
524 'action': action,
525 'action': action,
525 'line': line
526 'line': self._clean_line(line, command)
526 })
527 })
527
528
528 line = lineiter.next()
529 line = lineiter.next()
529
530
530 if line == self._newline_marker:
531 if self._newline_marker.match(line):
531 # we need to append to lines, since this is not
532 # we need to append to lines, since this is not
532 # counted in the line specs of diff
533 # counted in the line specs of diff
533 lines.append({
534 lines.append({
534 'old_lineno': '...',
535 'old_lineno': '...',
535 'new_lineno': '...',
536 'new_lineno': '...',
536 'action': 'context',
537 'action': 'context',
537 'line': line
538 'line': self._clean_line(line, command)
538 })
539 })
539
540
540 except StopIteration:
541 except StopIteration:
541 pass
542 pass
542 return chunks, stats
543 return chunks, stats
543
544
544 def _safe_id(self, idstring):
545 def _safe_id(self, idstring):
545 """Make a string safe for including in an id attribute.
546 """Make a string safe for including in an id attribute.
546
547
547 The HTML spec says that id attributes 'must begin with
548 The HTML spec says that id attributes 'must begin with
548 a letter ([A-Za-z]) and may be followed by any number
549 a letter ([A-Za-z]) and may be followed by any number
549 of letters, digits ([0-9]), hyphens ("-"), underscores
550 of letters, digits ([0-9]), hyphens ("-"), underscores
550 ("_"), colons (":"), and periods (".")'. These regexps
551 ("_"), colons (":"), and periods (".")'. These regexps
551 are slightly over-zealous, in that they remove colons
552 are slightly over-zealous, in that they remove colons
552 and periods unnecessarily.
553 and periods unnecessarily.
553
554
554 Whitespace is transformed into underscores, and then
555 Whitespace is transformed into underscores, and then
555 anything which is not a hyphen or a character that
556 anything which is not a hyphen or a character that
556 matches \w (alphanumerics and underscore) is removed.
557 matches \w (alphanumerics and underscore) is removed.
557
558
558 """
559 """
559 # Transform all whitespace to underscore
560 # Transform all whitespace to underscore
560 idstring = re.sub(r'\s', "_", '%s' % idstring)
561 idstring = re.sub(r'\s', "_", '%s' % idstring)
561 # Remove everything that is not a hyphen or a member of \w
562 # Remove everything that is not a hyphen or a member of \w
562 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
563 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
563 return idstring
564 return idstring
564
565
565 def prepare(self, inline_diff=True):
566 def prepare(self, inline_diff=True):
566 """
567 """
567 Prepare the passed udiff for HTML rendering. It'l return a list
568 Prepare the passed udiff for HTML rendering. It'l return a list
568 of dicts with diff information
569 of dicts with diff information
569 """
570 """
570 parsed = self._parser(inline_diff=inline_diff)
571 parsed = self._parser(inline_diff=inline_diff)
571 self.parsed = True
572 self.parsed = True
572 self.parsed_diff = parsed
573 self.parsed_diff = parsed
573 return parsed
574 return parsed
574
575
575 def as_raw(self, diff_lines=None):
576 def as_raw(self, diff_lines=None):
576 """
577 """
577 Returns raw string diff
578 Returns raw string diff
578 """
579 """
579 return self._diff
580 return self._diff
580 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
581 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
581
582
582 def as_html(self, table_class='code-difftable', line_class='line',
583 def as_html(self, table_class='code-difftable', line_class='line',
583 new_lineno_class='lineno old', old_lineno_class='lineno new',
584 new_lineno_class='lineno old', old_lineno_class='lineno new',
584 code_class='code', enable_comments=False, parsed_lines=None):
585 code_class='code', enable_comments=False, parsed_lines=None):
585 """
586 """
586 Return given diff as html table with customized css classes
587 Return given diff as html table with customized css classes
587 """
588 """
588 def _link_to_if(condition, label, url):
589 def _link_to_if(condition, label, url):
589 """
590 """
590 Generates a link if condition is meet or just the label if not.
591 Generates a link if condition is meet or just the label if not.
591 """
592 """
592
593
593 if condition:
594 if condition:
594 return '''<a href="%(url)s">%(label)s</a>''' % {
595 return '''<a href="%(url)s">%(label)s</a>''' % {
595 'url': url,
596 'url': url,
596 'label': label
597 'label': label
597 }
598 }
598 else:
599 else:
599 return label
600 return label
600 if not self.parsed:
601 if not self.parsed:
601 self.prepare()
602 self.prepare()
602
603
603 diff_lines = self.parsed_diff
604 diff_lines = self.parsed_diff
604 if parsed_lines:
605 if parsed_lines:
605 diff_lines = parsed_lines
606 diff_lines = parsed_lines
606
607
607 _html_empty = True
608 _html_empty = True
608 _html = []
609 _html = []
609 _html.append('''<table class="%(table_class)s">\n''' % {
610 _html.append('''<table class="%(table_class)s">\n''' % {
610 'table_class': table_class
611 'table_class': table_class
611 })
612 })
612
613
613 for diff in diff_lines:
614 for diff in diff_lines:
614 for line in diff['chunks']:
615 for line in diff['chunks']:
615 _html_empty = False
616 _html_empty = False
616 for change in line:
617 for change in line:
617 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
618 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
618 'lc': line_class,
619 'lc': line_class,
619 'action': change['action']
620 'action': change['action']
620 })
621 })
621 anchor_old_id = ''
622 anchor_old_id = ''
622 anchor_new_id = ''
623 anchor_new_id = ''
623 anchor_old = "%(filename)s_o%(oldline_no)s" % {
624 anchor_old = "%(filename)s_o%(oldline_no)s" % {
624 'filename': self._safe_id(diff['filename']),
625 'filename': self._safe_id(diff['filename']),
625 'oldline_no': change['old_lineno']
626 'oldline_no': change['old_lineno']
626 }
627 }
627 anchor_new = "%(filename)s_n%(oldline_no)s" % {
628 anchor_new = "%(filename)s_n%(oldline_no)s" % {
628 'filename': self._safe_id(diff['filename']),
629 'filename': self._safe_id(diff['filename']),
629 'oldline_no': change['new_lineno']
630 'oldline_no': change['new_lineno']
630 }
631 }
631 cond_old = (change['old_lineno'] != '...' and
632 cond_old = (change['old_lineno'] != '...' and
632 change['old_lineno'])
633 change['old_lineno'])
633 cond_new = (change['new_lineno'] != '...' and
634 cond_new = (change['new_lineno'] != '...' and
634 change['new_lineno'])
635 change['new_lineno'])
635 if cond_old:
636 if cond_old:
636 anchor_old_id = 'id="%s"' % anchor_old
637 anchor_old_id = 'id="%s"' % anchor_old
637 if cond_new:
638 if cond_new:
638 anchor_new_id = 'id="%s"' % anchor_new
639 anchor_new_id = 'id="%s"' % anchor_new
639 ###########################################################
640 ###########################################################
640 # OLD LINE NUMBER
641 # OLD LINE NUMBER
641 ###########################################################
642 ###########################################################
642 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
643 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
643 'a_id': anchor_old_id,
644 'a_id': anchor_old_id,
644 'olc': old_lineno_class
645 'olc': old_lineno_class
645 })
646 })
646
647
647 _html.append('''%(link)s''' % {
648 _html.append('''%(link)s''' % {
648 'link': _link_to_if(True, change['old_lineno'],
649 'link': _link_to_if(True, change['old_lineno'],
649 '#%s' % anchor_old)
650 '#%s' % anchor_old)
650 })
651 })
651 _html.append('''</td>\n''')
652 _html.append('''</td>\n''')
652 ###########################################################
653 ###########################################################
653 # NEW LINE NUMBER
654 # NEW LINE NUMBER
654 ###########################################################
655 ###########################################################
655
656
656 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
657 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
657 'a_id': anchor_new_id,
658 'a_id': anchor_new_id,
658 'nlc': new_lineno_class
659 'nlc': new_lineno_class
659 })
660 })
660
661
661 _html.append('''%(link)s''' % {
662 _html.append('''%(link)s''' % {
662 'link': _link_to_if(True, change['new_lineno'],
663 'link': _link_to_if(True, change['new_lineno'],
663 '#%s' % anchor_new)
664 '#%s' % anchor_new)
664 })
665 })
665 _html.append('''</td>\n''')
666 _html.append('''</td>\n''')
666 ###########################################################
667 ###########################################################
667 # CODE
668 # CODE
668 ###########################################################
669 ###########################################################
669 comments = '' if enable_comments else 'no-comment'
670 comments = '' if enable_comments else 'no-comment'
670 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
671 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
671 'cc': code_class,
672 'cc': code_class,
672 'inc': comments
673 'inc': comments
673 })
674 })
674 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
675 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
675 'code': change['line']
676 'code': change['line']
676 })
677 })
677
678
678 _html.append('''\t</td>''')
679 _html.append('''\t</td>''')
679 _html.append('''\n</tr>\n''')
680 _html.append('''\n</tr>\n''')
680 _html.append('''</table>''')
681 _html.append('''</table>''')
681 if _html_empty:
682 if _html_empty:
682 return None
683 return None
683 return ''.join(_html)
684 return ''.join(_html)
684
685
685 def stat(self):
686 def stat(self):
686 """
687 """
687 Returns tuple of added, and removed lines for this instance
688 Returns tuple of added, and removed lines for this instance
688 """
689 """
689 return self.adds, self.removes
690 return self.adds, self.removes
690
691
691
692
692 class InMemoryBundleRepo(bundlerepository):
693 class InMemoryBundleRepo(bundlerepository):
693 def __init__(self, ui, path, bundlestream):
694 def __init__(self, ui, path, bundlestream):
694 self._tempparent = None
695 self._tempparent = None
695 localrepo.localrepository.__init__(self, ui, path)
696 localrepo.localrepository.__init__(self, ui, path)
696 self.ui.setconfig('phases', 'publish', False)
697 self.ui.setconfig('phases', 'publish', False)
697
698
698 self.bundle = bundlestream
699 self.bundle = bundlestream
699
700
700 # dict with the mapping 'filename' -> position in the bundle
701 # dict with the mapping 'filename' -> position in the bundle
701 self.bundlefilespos = {}
702 self.bundlefilespos = {}
702
703
703
704
704 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
705 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
705 remote_compare=False, context=3, ignore_whitespace=False):
706 remote_compare=False, context=3, ignore_whitespace=False):
706 """
707 """
707 General differ between branches, bookmarks, revisions of two remote or
708 General differ between branches, bookmarks, revisions of two remote or
708 local but related repositories
709 local but related repositories
709
710
710 :param org_repo:
711 :param org_repo:
711 :param org_ref:
712 :param org_ref:
712 :param other_repo:
713 :param other_repo:
713 :type other_repo:
714 :type other_repo:
714 :type other_ref:
715 :type other_ref:
715 """
716 """
716
717
717 org_repo_scm = org_repo.scm_instance
718 org_repo_scm = org_repo.scm_instance
718 other_repo_scm = other_repo.scm_instance
719 other_repo_scm = other_repo.scm_instance
719
720
720 org_repo = org_repo_scm._repo
721 org_repo = org_repo_scm._repo
721 other_repo = other_repo_scm._repo
722 other_repo = other_repo_scm._repo
722
723
723 org_ref = org_ref[1]
724 org_ref = org_ref[1]
724 other_ref = other_ref[1]
725 other_ref = other_ref[1]
725
726
726 if org_repo == other_repo:
727 if org_repo == other_repo:
727 log.debug('running diff between %s@%s and %s@%s'
728 log.debug('running diff between %s@%s and %s@%s'
728 % (org_repo, org_ref, other_repo, other_ref))
729 % (org_repo, org_ref, other_repo, other_ref))
729 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
730 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
730 ignore_whitespace=ignore_whitespace, context=context)
731 ignore_whitespace=ignore_whitespace, context=context)
731 return _diff
732 return _diff
732
733
733 elif remote_compare:
734 elif remote_compare:
734 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
735 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
735 common, incoming, rheads = discovery_data
736 common, incoming, rheads = discovery_data
736 org_repo_peer = localrepo.locallegacypeer(org_repo.local())
737 org_repo_peer = localrepo.locallegacypeer(org_repo.local())
737 # create a bundle (uncompressed if other repo is not local)
738 # create a bundle (uncompressed if other repo is not local)
738 if org_repo_peer.capable('getbundle'):
739 if org_repo_peer.capable('getbundle'):
739 # disable repo hooks here since it's just bundle !
740 # disable repo hooks here since it's just bundle !
740 # patch and reset hooks section of UI config to not run any
741 # patch and reset hooks section of UI config to not run any
741 # hooks on fetching archives with subrepos
742 # hooks on fetching archives with subrepos
742 for k, _ in org_repo.ui.configitems('hooks'):
743 for k, _ in org_repo.ui.configitems('hooks'):
743 org_repo.ui.setconfig('hooks', k, None)
744 org_repo.ui.setconfig('hooks', k, None)
744
745
745 unbundle = org_repo.getbundle('incoming', common=common,
746 unbundle = org_repo.getbundle('incoming', common=common,
746 heads=None)
747 heads=None)
747
748
748 buf = BytesIO()
749 buf = BytesIO()
749 while True:
750 while True:
750 chunk = unbundle._stream.read(1024 * 4)
751 chunk = unbundle._stream.read(1024 * 4)
751 if not chunk:
752 if not chunk:
752 break
753 break
753 buf.write(chunk)
754 buf.write(chunk)
754
755
755 buf.seek(0)
756 buf.seek(0)
756 # replace chunked _stream with data that can do tell() and seek()
757 # replace chunked _stream with data that can do tell() and seek()
757 unbundle._stream = buf
758 unbundle._stream = buf
758
759
759 ui = make_ui('db')
760 ui = make_ui('db')
760 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
761 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
761 bundlestream=unbundle)
762 bundlestream=unbundle)
762
763
763 return ''.join(patch.diff(bundlerepo,
764 return ''.join(patch.diff(bundlerepo,
764 node1=other_repo[other_ref].node(),
765 node1=other_repo[other_ref].node(),
765 node2=org_repo[org_ref].node(),
766 node2=org_repo[org_ref].node(),
766 opts=opts))
767 opts=opts))
767
768
768 return '' No newline at end of file
769 return ''
@@ -1,78 +1,85 b''
1 import os
1 import os
2 import unittest
2 import unittest
3 from rhodecode.tests import *
3 from rhodecode.tests import *
4 from rhodecode.lib.diffs import DiffProcessor, NEW_FILENODE, DEL_FILENODE, \
4 from rhodecode.lib.diffs import DiffProcessor, NEW_FILENODE, DEL_FILENODE, \
5 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE
5 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE
6
6
7 dn = os.path.dirname
7 dn = os.path.dirname
8 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
8 FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures')
9
9
10 DIFF_FIXTURES = {
10 DIFF_FIXTURES = {
11 'hg_diff_add_single_binary_file.diff': [
11 'hg_diff_add_single_binary_file.diff': [
12 (u'US Warszawa.jpg', 'A', ['b', NEW_FILENODE]),
12 (u'US Warszawa.jpg', 'A', ['b', NEW_FILENODE]),
13 ],
13 ],
14 'hg_diff_mod_single_binary_file.diff': [
14 'hg_diff_mod_single_binary_file.diff': [
15 (u'US Warszawa.jpg', 'M', ['b', MOD_FILENODE]),
15 (u'US Warszawa.jpg', 'M', ['b', MOD_FILENODE]),
16 ],
16 ],
17 'hg_diff_del_single_binary_file.diff': [
17 'hg_diff_del_single_binary_file.diff': [
18 (u'US Warszawa.jpg', 'D', ['b', DEL_FILENODE]),
18 (u'US Warszawa.jpg', 'D', ['b', DEL_FILENODE]),
19 ],
19 ],
20 'hg_diff_binary_and_normal.diff': [
20 'hg_diff_binary_and_normal.diff': [
21 (u'img/baseline-10px.png', 'A', ['b', NEW_FILENODE]),
21 (u'img/baseline-10px.png', 'A', ['b', NEW_FILENODE]),
22 (u'js/jquery/hashgrid.js', 'A', [340, 0]),
22 (u'js/jquery/hashgrid.js', 'A', [340, 0]),
23 (u'index.html', 'M', [3, 2]),
23 (u'index.html', 'M', [3, 2]),
24 (u'less/docs.less', 'M', [34, 0]),
24 (u'less/docs.less', 'M', [34, 0]),
25 (u'less/scaffolding.less', 'M', [1, 3]),
25 (u'less/scaffolding.less', 'M', [1, 3]),
26 (u'readme.markdown', 'M', [1, 10]),
26 (u'readme.markdown', 'M', [1, 10]),
27 (u'img/baseline-20px.png', 'D', ['b', DEL_FILENODE]),
27 (u'img/baseline-20px.png', 'D', ['b', DEL_FILENODE]),
28 (u'js/global.js', 'D', [0, 75])
28 (u'js/global.js', 'D', [0, 75])
29 ],
29 ],
30 'hg_diff_chmod.diff': [
30 'hg_diff_chmod.diff': [
31 (u'file', 'M', ['b', CHMOD_FILENODE]),
31 (u'file', 'M', ['b', CHMOD_FILENODE]),
32 ],
32 ],
33 'hg_diff_rename_file.diff': [
33 'hg_diff_rename_file.diff': [
34 (u'file_renamed', 'M', ['b', RENAMED_FILENODE]),
34 (u'file_renamed', 'M', ['b', RENAMED_FILENODE]),
35 ],
35 ],
36 'git_diff_chmod.diff': [
36 'git_diff_chmod.diff': [
37 (u'work-horus.xls', 'M', ['b', CHMOD_FILENODE]),
37 (u'work-horus.xls', 'M', ['b', CHMOD_FILENODE]),
38 ],
38 ],
39 'git_diff_rename_file.diff': [
39 'git_diff_rename_file.diff': [
40 (u'file.xls', 'M', ['b', RENAMED_FILENODE]),
40 (u'file.xls', 'M', ['b', RENAMED_FILENODE]),
41 ],
41 ],
42 'git_diff_mod_single_binary_file.diff': [
42 'git_diff_mod_single_binary_file.diff': [
43 ('US Warszawa.jpg', 'M', ['b', MOD_FILENODE])
43 ('US Warszawa.jpg', 'M', ['b', MOD_FILENODE])
44
44
45 ],
45 ],
46 'git_diff_binary_and_normal.diff': [
46 'git_diff_binary_and_normal.diff': [
47 (u'img/baseline-10px.png', 'A', ['b', NEW_FILENODE]),
47 (u'img/baseline-10px.png', 'A', ['b', NEW_FILENODE]),
48 (u'js/jquery/hashgrid.js', 'A', [340, 0]),
48 (u'js/jquery/hashgrid.js', 'A', [340, 0]),
49 (u'index.html', 'M', [3, 2]),
49 (u'index.html', 'M', [3, 2]),
50 (u'less/docs.less', 'M', [34, 0]),
50 (u'less/docs.less', 'M', [34, 0]),
51 (u'less/scaffolding.less', 'M', [1, 3]),
51 (u'less/scaffolding.less', 'M', [1, 3]),
52 (u'readme.markdown', 'M', [1, 10]),
52 (u'readme.markdown', 'M', [1, 10]),
53 (u'img/baseline-20px.png', 'D', ['b', DEL_FILENODE]),
53 (u'img/baseline-20px.png', 'D', ['b', DEL_FILENODE]),
54 (u'js/global.js', 'D', [0, 75])
54 (u'js/global.js', 'D', [0, 75])
55 ],
55 ],
56 'diff_with_diff_data.diff': [
57 (u'vcs/backends/base.py', 'M', [18, 2]),
58 (u'vcs/backends/git/repository.py', 'M', [46, 15]),
59 (u'vcs/backends/hg.py', 'M', [22, 3]),
60 (u'vcs/tests/test_git.py', 'M', [5, 5]),
61 (u'vcs/tests/test_repository.py', 'M', [174, 2])
62 ],
56 # 'large_diff.diff': [
63 # 'large_diff.diff': [
57 #
64 #
58 # ],
65 # ],
59
66
60
67
61 }
68 }
62
69
63
70
64 def _diff_checker(fixture):
71 def _diff_checker(fixture):
65 with open(os.path.join(FIXTURES, fixture)) as f:
72 with open(os.path.join(FIXTURES, fixture)) as f:
66 diff = f.read()
73 diff = f.read()
67
74
68 diff_proc = DiffProcessor(diff)
75 diff_proc = DiffProcessor(diff)
69 diff_proc_d = diff_proc.prepare()
76 diff_proc_d = diff_proc.prepare()
70 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
77 data = [(x['filename'], x['operation'], x['stats']) for x in diff_proc_d]
71 expected_data = DIFF_FIXTURES[fixture]
78 expected_data = DIFF_FIXTURES[fixture]
72
79
73 assert expected_data == data
80 assert expected_data == data
74
81
75
82
76 def test_parse_diff():
83 def test_parse_diff():
77 for fixture in DIFF_FIXTURES:
84 for fixture in DIFF_FIXTURES:
78 yield _diff_checker, fixture
85 yield _diff_checker, fixture
General Comments 0
You need to be logged in to leave comments. Login now