##// 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()
@@ -160,8 +160,8 b' class DiffProcessor(object):'
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
@@ -348,6 +348,12 b' class DiffProcessor(object):'
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
@@ -489,14 +495,9 b' class DiffProcessor(object):'
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
@@ -515,26 +516,26 b' class DiffProcessor(object):'
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:
@@ -53,6 +53,13 b' DIFF_FIXTURES = {'
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 # ],
General Comments 0
You need to be logged in to leave comments. Login now