changeset 911:772280aed751

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.
author Ronny Voelker <ronny.voelker@googlemail.com>
date Sun, 01 Jan 2012 15:59:15 +0100
parents 312f36a425f0
children adf4a0890cc5
files hgsubversion/pushmod.py hgsubversion/svnwrap/common.py hgsubversion/svnwrap/subvertpy_wrapper.py hgsubversion/svnwrap/svn_swig_wrapper.py tests/run.py tests/test_push_autoprops.py tests/test_util.py
diffstat 7 files changed, 189 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- 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] + [''])
--- 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
+
--- 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):
         """
--- 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
--- 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
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__])
+
--- 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)