Skip to content

Commit

Permalink
Use svn status --xml for reliability
Browse files Browse the repository at this point in the history
This handles svn:externals correctly.

Fixes #45.
  • Loading branch information
mgedmin committed Feb 6, 2015
1 parent 0893381 commit 77b0f51
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
0.23 (unreleased)
-----------------

* More reliable svn status parsing; now handles svn externals (`issue #45
<https://github.com/mgedmin/check-manifest/issues/45>`__).


0.22 (2014-12-23)
-----------------
Expand Down
60 changes: 53 additions & 7 deletions check_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import tempfile
import zipfile
from contextlib import contextmanager, closing
from xml.etree import cElementTree as ET

try:
import ConfigParser
Expand Down Expand Up @@ -126,7 +127,7 @@ def __init__(self, command, status, output):
command, status, output))


def run(command, encoding=None):
def run(command, encoding=None, decode=True):
"""Run a command [cmd, arg1, arg2, ...].
Returns the output (stdout + stderr).
Expand All @@ -140,7 +141,9 @@ def run(command, encoding=None):
stderr=subprocess.STDOUT)
except OSError as e:
raise Failure("could not run %s: %s" % (command, e))
output = pipe.communicate()[0].decode(encoding)
output = pipe.communicate()[0]
if decode:
output = output.decode(encoding)
status = pipe.wait()
if status != 0:
raise CommandFailed(command, status, output)
Expand Down Expand Up @@ -302,12 +305,55 @@ def get_versioned_files():
class Subversion(VCS):
metadata_name = '.svn'

@staticmethod
def get_versioned_files():
@classmethod
def get_versioned_files(cls):
"""List all files under SVN control in the current directory."""
output = run(['svn', 'st', '-vq'])
return sorted(line[12:].split(None, 3)[-1]
for line in output.splitlines()[1:])
output = run(['svn', 'st', '-vq', '--xml'], decode=False)
tree = ET.XML(output)
return sorted(entry.get('path') for entry in tree.findall('.//entry')
if cls.is_interesting(entry))

@staticmethod
def is_interesting(entry):
"""Is this entry interesting?
``entry`` is an XML node representing one entry of the svn status
XML output. It looks like this::
<entry path="unchanged.txt">
<wc-status item="normal" revision="1" props="none">
<commit revision="1">
<author>mg</author>
<date>2015-02-06T07:52:38.163516Z</date>
</commit>
</wc-status>
</entry>
<entry path="added-but-not-committed.txt">
<wc-status item="added" revision="-1" props="none"></wc-status>
</entry>
<entry path="ext">
<wc-status item="external" props="none"></wc-status>
</entry>
<entry path="unknown.txt">
<wc-status props="none" item="unversioned"></wc-status>
</entry>
"""
if entry.get('path') == '.':
return False
status = entry.find('wc-status')
if status is None:
warning('svn status --xml parse error: <entry path="%s"> without'
' <wc-status>' % entry.get('path'))
return False
# For SVN externals we get two entries: one mentioning the
# existence of the external, and one about the status of the external.
if status.get('item') in ('unversioned', 'external'):
return False
return True


def detect_vcs():
Expand Down
31 changes: 30 additions & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import zipfile
from contextlib import closing
from io import BytesIO
from xml.etree import cElementTree as ET

try:
import unittest2 as unittest # Python 2.6
Expand Down Expand Up @@ -914,8 +915,21 @@ def _commit(self):
class TestSvn(VCSMixin, unittest.TestCase):
vcs = SvnHelper()

def test_svn_externals(self):
from check_manifest import get_vcs_files
self.vcs._run('svnadmin', 'create', 'repo2')
repo2_url = 'file:///' + os.path.abspath('repo2').replace(os.path.sep, '/')
self.vcs._init_vcs()
self.vcs._run('svn', 'propset', 'svn:externals', 'ext %s' % repo2_url, '.')
self.vcs._run('svn', 'up')
self._create_files(['a.txt', 'ext/b.txt'])
self.vcs._run('svn', 'add', 'a.txt', 'ext/b.txt')
j = os.path.join
self.assertEqual(get_vcs_files(),
['a.txt', 'ext', j('ext', 'b.txt')])


class TestUserInterface(unittest.TestCase):
class UIMixin(object):

def setUp(self):
import check_manifest
Expand All @@ -931,6 +945,21 @@ def tearDown(self):
sys.stdout = self.real_stdout
check_manifest.VERBOSE = self.old_VERBOSE


class TestSvnExtraErrors(UIMixin, unittest.TestCase):

def test_svn_xml_parsing_warning(self):
from check_manifest import Subversion
entry = ET.XML('<entry path="foo/bar.txt"></entry>')
self.assertFalse(Subversion.is_interesting(entry))
self.assertEqual(
sys.stderr.getvalue(),
'svn status --xml parse error: <entry path="foo/bar.txt">'
' without <wc-status>\n')


class TestUserInterface(UIMixin, unittest.TestCase):

def test_info(self):
import check_manifest
check_manifest.VERBOSE = False
Expand Down

0 comments on commit 77b0f51

Please sign in to comment.