# HG changeset patch # User Ronny Voelker # Date 1325429955 -3600 # Node ID 772280aed7513b672aa8ba060750f52518e1b20e # Parent 312f36a425f01328c9f36b831019c30b6f4957fc Honor SVN auto-props (solves issue #186) The auto-props are read from the users subversion configuration file (~/.subversion/config on posix). System-wide configuration files are not taken into account. The implementation completely bypasses the subversion bindings, because the current bindings provide little support for this functionality. diff --git a/hgsubversion/pushmod.py b/hgsubversion/pushmod.py --- a/hgsubversion/pushmod.py +++ b/hgsubversion/pushmod.py @@ -133,6 +133,10 @@ def commit(ui, repo, rev_ctx, meta, base # this kind of renames: a -> b, b -> c copies[file] = renamed[0] base_data = parent[renamed[0]].data() + else: + autoprops = svn.autoprops_config.properties(file) + if autoprops: + props.setdefault(file, {}).update(autoprops) action = 'add' dirname = '/'.join(file.split('/')[:-1] + ['']) diff --git a/hgsubversion/svnwrap/common.py b/hgsubversion/svnwrap/common.py --- a/hgsubversion/svnwrap/common.py +++ b/hgsubversion/svnwrap/common.py @@ -8,6 +8,9 @@ import tempfile import urlparse import urllib import collections +import fnmatch +import ConfigParser +import sys class SubversionRepoCanNotReplay(Exception): """Exception raised when the svn server is too old to have replay. @@ -78,3 +81,66 @@ class Revision(tuple): def __str__(self): return 'r%d by %s' % (self.revnum, self.author) + + +_svn_config_dir = None + + +class AutoPropsConfig(object): + """Provides the subversion auto-props functionality + when pushing new files. + """ + def __init__(self, config_dir=None): + config_file = config_file_path(config_dir) + self.config = ConfigParser.RawConfigParser() + self.config.read([config_file]) + + def properties(self, file): + """Returns a dictionary of the auto-props applicable for file. + Takes enable-auto-props into account. + """ + properties = {} + if self.autoprops_enabled(): + for pattern,prop_list in self.config.items('auto-props'): + if fnmatch.fnmatchcase(os.path.basename(file), pattern): + properties.update(parse_autoprops(prop_list)) + return properties + + def autoprops_enabled(self): + return (self.config.has_option('miscellany', 'enable-auto-props') + and self.config.getboolean( 'miscellany', 'enable-auto-props') + and self.config.has_section('auto-props')) + + +def config_file_path(config_dir): + if config_dir == None: + global _svn_config_dir + config_dir = _svn_config_dir + if config_dir == None: + if sys.platform == 'win32': + config_dir = os.path.join(os.environ['APPDATA'], 'Subversion') + else: + config_dir = os.path.join(os.environ['HOME'], '.subversion') + return os.path.join(config_dir, 'config') + + +def parse_autoprops(prop_list): + """Parses a string of autoprops and returns a dictionary of + the results. + Emulates the parsing of core.auto_props_enumerator. + """ + def unquote(s): + if len(s)>1 and s[0] in ['"', "'"] and s[0]==s[-1]: + return s[1:-1] + return s + + properties = {} + for prop in prop_list.split(';'): + if '=' in prop: + prop, value = prop.split('=',1) + value = unquote(value.strip()) + else: + value = '' + properties[prop.strip()] = value + return properties + diff --git a/hgsubversion/svnwrap/subvertpy_wrapper.py b/hgsubversion/svnwrap/subvertpy_wrapper.py --- a/hgsubversion/svnwrap/subvertpy_wrapper.py +++ b/hgsubversion/svnwrap/subvertpy_wrapper.py @@ -191,6 +191,7 @@ class SubversionRepo(object): # expects unquoted paths self.subdir = urllib.unquote(self.subdir) self.hasdiff3 = True + self.autoprops_config = common.AutoPropsConfig() def init_ra_and_client(self): """ diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py --- a/hgsubversion/svnwrap/svn_swig_wrapper.py +++ b/hgsubversion/svnwrap/svn_swig_wrapper.py @@ -184,6 +184,7 @@ class SubversionRepo(object): # expects unquoted paths self.subdir = urllib.unquote(self.subdir) self.hasdiff3 = True + self.autoprops_config = common.AutoPropsConfig() def init_ra_and_client(self): """Initializes the RA and client layers, because sometimes getting diff --git a/tests/run.py b/tests/run.py --- a/tests/run.py +++ b/tests/run.py @@ -23,6 +23,7 @@ def tests(): import test_push_renames import test_push_dirs import test_push_eol + import test_push_autoprops import test_rebuildmeta import test_single_dir_clone import test_svnwrap diff --git a/tests/test_push_autoprops.py b/tests/test_push_autoprops.py new file mode 100644 --- /dev/null +++ b/tests/test_push_autoprops.py @@ -0,0 +1,107 @@ +import subprocess +import sys +import unittest +import os + +import test_util + +from hgsubversion import svnwrap + +class PushAutoPropsTests(test_util.TestBase): + def setUp(self): + test_util.TestBase.setUp(self) + repo, self.repo_path = self.load_and_fetch('emptyrepo.svndump') + + def test_push_honors_svn_autoprops(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + changes = [('test.py', 'test.py', 'echo hallo')] + self.commitchanges(changes) + self.pushrevisions(True) + prop_val = test_util.svnpropget( + self.repo_path, "trunk/test.py", 'test:prop') + self.assertEqual('success', prop_val) + + +class AutoPropsConfigTest(test_util.TestBase): + def test_use_autoprops_for_matching_file_when_enabled(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({ 'test:prop': 'success'}, props) + + def new_autoprops_config(self): + return svnwrap.AutoPropsConfig(self.config_dir) + + def test_ignore_nonexisting_config(self): + config_file = os.path.join(self.config_dir, 'config') + os.remove(config_file) + self.assertTrue(not os.path.exists(config_file)) + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({}, props) + + def test_ignore_autoprops_when_file_doesnt_match(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + props = self.new_autoprops_config().properties('xxx/test.sh') + self.assertEqual({}, props) + + def test_ignore_autoprops_when_disabled(self): + self.setup_svn_config( + "[miscellany]\n" + "#enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({}, props) + + def test_combine_properties_of_multiple_matches(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n" + "test.* = test:prop2=success\n") + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({ + 'test:prop': 'success', 'test:prop2': 'success'}, props) + + +class ParseAutoPropsTests(test_util.TestBase): + def test_property_value_is_optional(self): + props = svnwrap.parse_autoprops("svn:executable") + self.assertEqual({'svn:executable': ''}, props) + props = svnwrap.parse_autoprops("svn:executable=") + self.assertEqual({'svn:executable': ''}, props) + + def test_property_value_may_be_quoted(self): + props = svnwrap.parse_autoprops("svn:eol-style=\" native \"") + self.assertEqual({'svn:eol-style': ' native '}, props) + props = svnwrap.parse_autoprops("svn:eol-style=' native '") + self.assertEqual({'svn:eol-style': ' native '}, props) + + def test_surrounding_whitespaces_are_ignored(self): + props = svnwrap.parse_autoprops(" svn:eol-style = native ") + self.assertEqual({'svn:eol-style': 'native'}, props) + + def test_multiple_properties_are_separated_by_semicolon(self): + props = svnwrap.parse_autoprops( + "svn:eol-style=native;svn:executable=true\n") + self.assertEqual({ + 'svn:eol-style': 'native', + 'svn:executable': 'true'}, + props) + + +def suite(): + return unittest.findTestCases(sys.modules[__name__]) + diff --git a/tests/test_util.py b/tests/test_util.py --- a/tests/test_util.py +++ b/tests/test_util.py @@ -38,6 +38,7 @@ except AttributeError: SkipTest = None from hgsubversion import util +from hgsubversion import svnwrap # Documentation for Subprocess.Popen() says: # "Note that on Windows, you cannot set close_fds to true and @@ -260,6 +261,10 @@ class TestBase(unittest.TestCase): self.wc_path = '%s/testrepo_wc' % self.tmpdir self.svn_wc = None + self.config_dir = self.tmpdir + svnwrap.common._svn_config_dir = self.config_dir + self.setup_svn_config('') + # Previously, we had a MockUI class that wrapped ui, and giving access # to the stream. The ui.pushbuffer() and ui.popbuffer() can be used # instead. Using the regular UI class, with all stderr redirected to @@ -268,6 +273,10 @@ class TestBase(unittest.TestCase): self.patch = (ui.ui.write_err, ui.ui.write) setattr(ui.ui, self.patch[0].func_name, self.patch[1]) + def setup_svn_config(self, config): + with open(self.config_dir + '/config', 'w') as c: + c.write(config) + def _makerepopath(self): self.repocount += 1 return '%s/testrepo-%d' % (self.tmpdir, self.repocount)