diff --git a/vcs/backends/base.py b/vcs/backends/base.py index 212267ca23949807b8d89fa8ca495827dcfab3b1..ad17f16634da602503ed4ddd7cdd2e1ccdf4bed4 100644 --- a/vcs/backends/base.py +++ b/vcs/backends/base.py @@ -54,6 +54,7 @@ class BaseRepository(object): """ scm = None DEFAULT_BRANCH_NAME = None + EMPTY_CHANGESET = '0' * 40 def __init__(self, repo_path, create=False, **kwargs): """ @@ -204,6 +205,23 @@ class BaseRepository(object): """ raise NotImplementedError + def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, + context=3): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + ``rev2`` since ``rev1``. + + :param rev1: Entry point from which diff is shown. Can be + ``self.EMPTY_CHANGESET`` - in this case, patch showing all + the changes since empty state of the repository until ``rev2`` + :param rev2: Until which revision changes should be shown. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + """ + raise NotImplementedError + # ========== # # COMMIT API # # ========== # @@ -341,7 +359,6 @@ class BaseChangeset(object): otherwise; trying to access this attribute while there is no changesets would raise ``EmptyRepositoryError`` """ - def __str__(self): return '<%s at %s:%s>' % (self.__class__.__name__, self.revision, self.short_id) @@ -591,7 +608,6 @@ class BaseChangeset(object): return data - class BaseWorkdir(object): """ Working directory representation of single repository. diff --git a/vcs/backends/git/repository.py b/vcs/backends/git/repository.py index 8b9d1247fdee44e7a021b80e4965d8609cfd5720..e9f04e74dedd2f57417eb91dd2f4f7c61ec7e097 100644 --- a/vcs/backends/git/repository.py +++ b/vcs/backends/git/repository.py @@ -12,6 +12,7 @@ import os import re import time +import inspect import posixpath from dulwich.repo import Repo, NotGitRepository #from dulwich.config import ConfigFile @@ -101,21 +102,6 @@ class GitRepository(BaseRepository): "stderr:\n%s" % (cmd, se)) return so, se - def _get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, - context=3): - rev1 = self._get_revision(rev1) - rev2 = self._get_revision(rev2) - - if ignore_whitespace: - cmd = 'diff -U%s -w %s %s' % (context, rev1, rev2) - else: - cmd = 'diff -U%s %s %s' % (context, rev1, rev2) - if path: - cmd += ' -- "%s"' % path - so, se = self.run_git_command(cmd) - - return so - def _check_url(self, url): """ Functon will check given url and try to verify if it's a valid @@ -322,6 +308,8 @@ class GitRepository(BaseRepository): Returns ``GitChangeset`` object representing commit from git repository at the given revision or head (most recent commit) if None given. """ + if isinstance(revision, GitChangeset): + return revision revision = self._get_revision(revision) changeset = GitChangeset(repository=self, revision=revision) return changeset @@ -398,6 +386,49 @@ class GitRepository(BaseRepository): for rev in revs: yield self.get_changeset(rev) + def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, + context=3): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + ``rev2`` since ``rev1``. + + :param rev1: Entry point from which diff is shown. Can be + ``self.EMPTY_CHANGESET`` - in this case, patch showing all + the changes since empty state of the repository until ``rev2`` + :param rev2: Until which revision changes should be shown. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + """ + flags = ['-U%s' % context] + if ignore_whitespace: + flags.append('-w') + + if rev1 == self.EMPTY_CHANGESET: + rev2 = self.get_changeset(rev2).raw_id + cmd = ' '.join(['show'] + flags + [rev2]) + else: + rev1 = self.get_changeset(rev1).raw_id + rev2 = self.get_changeset(rev2).raw_id + cmd = ' '.join(['diff'] + flags + [rev1, rev2]) + + if path: + cmd += ' -- "%s"' % path + stdout, stderr = self.run_git_command(cmd) + # If we used 'show' command, strip first few lines (until actual diff + # starts) + if rev1 == self.EMPTY_CHANGESET: + lines = stdout.splitlines() + x = 0 + for line in lines: + if line.startswith('diff'): + break + x += 1 + # Append new line just like 'diff' command do + stdout = '\n'.join(lines[x:]) + '\n' + return stdout + @LazyProperty def in_memory_changeset(self): """ diff --git a/vcs/backends/hg.py b/vcs/backends/hg.py index f1f9f95e4d476ab01d8e7b02a8b59034c0740a3b..b7d63c552c39b2f8aaec17ef46551369c8b8e793 100644 --- a/vcs/backends/hg.py +++ b/vcs/backends/hg.py @@ -256,13 +256,32 @@ class MercurialRepository(BaseRepository): return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1] - def _get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, + def get_diff(self, rev1, rev2, path='', ignore_whitespace=False, context=3): + """ + Returns (git like) *diff*, as plain text. Shows changes introduced by + ``rev2`` since ``rev1``. + + :param rev1: Entry point from which diff is shown. Can be + ``self.EMPTY_CHANGESET`` - in this case, patch showing all + the changes since empty state of the repository until ``rev2`` + :param rev2: Until which revision changes should be shown. + :param ignore_whitespace: If set to ``True``, would not show whitespace + changes. Defaults to ``False``. + :param context: How many lines before/after changed lines should be + shown. Defaults to ``3``. + """ + # Check if given revisions are present at repository (may raise + # ChangesetDoesNotExistError) + if rev1 != self.EMPTY_CHANGESET: + self.get_changeset(rev1) + self.get_changeset(rev2) + file_filter = match(self.path, '', [path]) - return patch.diff(self._repo, rev1, rev2, match=file_filter, + return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter, opts=diffopts(git=True, ignorews=ignore_whitespace, - context=context)) + context=context))) def _check_url(self, url): """ diff --git a/vcs/tests/test_git.py b/vcs/tests/test_git.py index 30da035a2a35c3dca14064778e97188b6d4ce5d6..d4b82b9e612af8bb5bf490a827377c7c2567735a 100644 --- a/vcs/tests/test_git.py +++ b/vcs/tests/test_git.py @@ -639,19 +639,19 @@ class GitSpecificWithRepoTest(BackendTestMixin, unittest.TestCase): def test_get_diff_runs_git_command_with_hashes(self): self.repo.run_git_command = mock.Mock(return_value=['', '']) - self.repo._get_diff(0, 1) + self.repo.get_diff(0, 1) self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s' % (3, self.repo._get_revision(0), self.repo._get_revision(1))) def test_get_diff_runs_git_command_with_str_hashes(self): self.repo.run_git_command = mock.Mock(return_value=['', '']) - self.repo._get_diff('0' * 40, 1) - self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s' % - (3, self.repo._get_revision(0), self.repo._get_revision(1))) + self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1) + self.repo.run_git_command.assert_called_once_with('show -U%s %s' % + (3, self.repo._get_revision(1))) def test_get_diff_runs_git_command_with_path_if_its_given(self): self.repo.run_git_command = mock.Mock(return_value=['', '']) - self.repo._get_diff(0, 1, 'foo') + self.repo.get_diff(0, 1, 'foo') self.repo.run_git_command.assert_called_once_with('diff -U%s %s %s -- "foo"' % (3, self.repo._get_revision(0), self.repo._get_revision(1))) diff --git a/vcs/tests/test_repository.py b/vcs/tests/test_repository.py index e34033e29fa9b3d3366b723beab129cee73869b9..b6e3f419778d6009229e9108824acaf83eea1784 100644 --- a/vcs/tests/test_repository.py +++ b/vcs/tests/test_repository.py @@ -1,9 +1,12 @@ from __future__ import with_statement +import datetime from base import BackendTestMixin from conf import SCM_TESTS +from conf import TEST_USER_CONFIG_FILE +from vcs.nodes import FileNode from vcs.utils.compat import unittest +from vcs.exceptions import ChangesetDoesNotExistError -from conf import TEST_USER_CONFIG_FILE class RepositoryBaseTest(BackendTestMixin): recreate_repo_per_test = False @@ -29,6 +32,176 @@ class RepositoryBaseTest(BackendTestMixin): 'foo.bar@example.com') + +class RepositoryGetDiffTest(BackendTestMixin): + + @classmethod + def _get_commits(cls): + commits = [ + { + 'message': 'Initial commit', + 'author': 'Joe Doe ', + 'date': datetime.datetime(2010, 1, 1, 20), + 'added': [ + FileNode('foobar', content='foobar'), + FileNode('foobar2', content='foobar2'), + ], + }, + { + 'message': 'Changed foobar, added foobar3', + 'author': 'Jane Doe ', + 'date': datetime.datetime(2010, 1, 1, 21), + 'added': [ + FileNode('foobar3', content='foobar3'), + ], + 'changed': [ + FileNode('foobar', 'FOOBAR'), + ], + }, + { + 'message': 'Removed foobar, changed foobar3', + 'author': 'Jane Doe ', + 'date': datetime.datetime(2010, 1, 1, 22), + 'changed': [ + FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'), + ], + 'removed': [FileNode('foobar')], + }, + ] + return commits + + def test_raise_for_wrong(self): + with self.assertRaises(ChangesetDoesNotExistError): + self.repo.get_diff('a' * 40, 'b' * 40) + +class GitRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase): + backend_alias = 'git' + + def test_initial_commit_diff(self): + initial_rev = self.repo.revisions[0] + self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar +new file mode 100644 +index 0000000..f6ea049 +--- /dev/null ++++ b/foobar +@@ -0,0 +1 @@ ++foobar +\ No newline at end of file +diff --git a/foobar2 b/foobar2 +new file mode 100644 +index 0000000..e8c9d6b +--- /dev/null ++++ b/foobar2 +@@ -0,0 +1 @@ ++foobar2 +\ No newline at end of file +''') + + def test_second_changeset_diff(self): + revs = self.repo.revisions + self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar +index f6ea049..389865b 100644 +--- a/foobar ++++ b/foobar +@@ -1 +1 @@ +-foobar +\ No newline at end of file ++FOOBAR +\ No newline at end of file +diff --git a/foobar3 b/foobar3 +new file mode 100644 +index 0000000..c11c37d +--- /dev/null ++++ b/foobar3 +@@ -0,0 +1 @@ ++foobar3 +\ No newline at end of file +''') + + def test_third_changeset_diff(self): + revs = self.repo.revisions + self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar +deleted file mode 100644 +index 389865b..0000000 +--- a/foobar ++++ /dev/null +@@ -1 +0,0 @@ +-FOOBAR +\ No newline at end of file +diff --git a/foobar3 b/foobar3 +index c11c37d..f932447 100644 +--- a/foobar3 ++++ b/foobar3 +@@ -1 +1,3 @@ +-foobar3 +\ No newline at end of file ++FOOBAR ++FOOBAR ++FOOBAR +''') + + +class HgRepositoryGetDiffTest(RepositoryGetDiffTest, unittest.TestCase): + backend_alias = 'hg' + + def test_initial_commit_diff(self): + initial_rev = self.repo.revisions[0] + self.assertEqual(self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev), '''diff --git a/foobar b/foobar +new file mode 100755 +--- /dev/null ++++ b/foobar +@@ -0,0 +1,1 @@ ++foobar +\ No newline at end of file +diff --git a/foobar2 b/foobar2 +new file mode 100755 +--- /dev/null ++++ b/foobar2 +@@ -0,0 +1,1 @@ ++foobar2 +\ No newline at end of file +''') + + def test_second_changeset_diff(self): + revs = self.repo.revisions + self.assertEqual(self.repo.get_diff(revs[0], revs[1]), '''diff --git a/foobar b/foobar +--- a/foobar ++++ b/foobar +@@ -1,1 +1,1 @@ +-foobar +\ No newline at end of file ++FOOBAR +\ No newline at end of file +diff --git a/foobar3 b/foobar3 +new file mode 100755 +--- /dev/null ++++ b/foobar3 +@@ -0,0 +1,1 @@ ++foobar3 +\ No newline at end of file +''') + + def test_third_changeset_diff(self): + revs = self.repo.revisions + self.assertEqual(self.repo.get_diff(revs[1], revs[2]), '''diff --git a/foobar b/foobar +deleted file mode 100755 +--- a/foobar ++++ /dev/null +@@ -1,1 +0,0 @@ +-FOOBAR +\ No newline at end of file +diff --git a/foobar3 b/foobar3 +--- a/foobar3 ++++ b/foobar3 +@@ -1,1 +1,3 @@ +-foobar3 +\ No newline at end of file ++FOOBAR ++FOOBAR ++FOOBAR +''') + + # For each backend create test case class for alias in SCM_TESTS: attrs = { @@ -38,7 +211,6 @@ for alias in SCM_TESTS: bases = (RepositoryBaseTest, unittest.TestCase) globals()[cls_name] = type(cls_name, bases, attrs) - if __name__ == '__main__': unittest.main()