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