Commits

ronvoe committed 772280a

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.

Comments (0)

Files changed (7)

hgsubversion/pushmod.py

                     # 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] + [''])

hgsubversion/svnwrap/common.py

 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.
 
     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
+

hgsubversion/svnwrap/subvertpy_wrapper.py

         # expects unquoted paths
         self.subdir = urllib.unquote(self.subdir)
         self.hasdiff3 = True
+        self.autoprops_config = common.AutoPropsConfig()
 
     def init_ra_and_client(self):
         """

hgsubversion/svnwrap/svn_swig_wrapper.py

         # 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
     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

tests/test_push_autoprops.py

+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__])
+

tests/test_util.py

             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
         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
         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)