diff --git a/rhodecode/lib/vcs/backends/git/config.py b/rhodecode/lib/vcs/backends/git/config.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/backends/git/config.py @@ -0,0 +1,347 @@ +# config.py - Reading and writing Git config files +# Copyright (C) 2011 Jelmer Vernooij +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your option) a later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +"""Reading and writing Git configuration files. + +TODO: + * preserve formatting when updating configuration files + * treat subsection names as case-insensitive for [branch.foo] style + subsections +""" + +# Taken from dulwich not yet released 0.8.3 version (until it is actually +# released) + +import errno +import os +import re + +from dulwich.file import GitFile + + +class Config(object): + """A Git configuration.""" + + def get(self, section, name): + """Retrieve the contents of a configuration setting. + + :param section: Tuple with section name and optional subsection namee + :param subsection: Subsection name + :return: Contents of the setting + :raise KeyError: if the value is not set + """ + raise NotImplementedError(self.get) + + def get_boolean(self, section, name, default=None): + """Retrieve a configuration setting as boolean. + + :param section: Tuple with section name and optional subsection namee + :param name: Name of the setting, including section and possible + subsection. + :return: Contents of the setting + :raise KeyError: if the value is not set + """ + try: + value = self.get(section, name) + except KeyError: + return default + if value.lower() == "true": + return True + elif value.lower() == "false": + return False + raise ValueError("not a valid boolean string: %r" % value) + + def set(self, section, name, value): + """Set a configuration value. + + :param name: Name of the configuration value, including section + and optional subsection + :param: Value of the setting + """ + raise NotImplementedError(self.set) + + +class ConfigDict(Config): + """Git configuration stored in a dictionary.""" + + def __init__(self, values=None): + """Create a new ConfigDict.""" + if values is None: + values = {} + self._values = values + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self._values) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + other._values == self._values) + + @classmethod + def _parse_setting(cls, name): + parts = name.split(".") + if len(parts) == 3: + return (parts[0], parts[1], parts[2]) + else: + return (parts[0], None, parts[1]) + + def get(self, section, name): + if isinstance(section, basestring): + section = (section, ) + if len(section) > 1: + try: + return self._values[section][name] + except KeyError: + pass + return self._values[(section[0],)][name] + + def set(self, section, name, value): + if isinstance(section, basestring): + section = (section, ) + self._values.setdefault(section, {})[name] = value + + +def _format_string(value): + if (value.startswith(" ") or + value.startswith("\t") or + value.endswith(" ") or + value.endswith("\t")): + return '"%s"' % _escape_value(value) + return _escape_value(value) + + +def _parse_string(value): + value = value.strip() + ret = [] + block = [] + in_quotes = False + for c in value: + if c == "\"": + in_quotes = (not in_quotes) + ret.append(_unescape_value("".join(block))) + block = [] + elif c in ("#", ";") and not in_quotes: + # the rest of the line is a comment + break + else: + block.append(c) + + if in_quotes: + raise ValueError("value starts with quote but lacks end quote") + + ret.append(_unescape_value("".join(block)).rstrip()) + + return "".join(ret) + + +def _unescape_value(value): + """Unescape a value.""" + def unescape(c): + return { + "\\\\": "\\", + "\\\"": "\"", + "\\n": "\n", + "\\t": "\t", + "\\b": "\b", + }[c.group(0)] + return re.sub(r"(\\.)", unescape, value) + + +def _escape_value(value): + """Escape a value.""" + return value.replace("\\", "\\\\").replace("\n", "\\n")\ + .replace("\t", "\\t").replace("\"", "\\\"") + + +def _check_variable_name(name): + for c in name: + if not c.isalnum() and c != '-': + return False + return True + + +def _check_section_name(name): + for c in name: + if not c.isalnum() and c not in ('-', '.'): + return False + return True + + +def _strip_comments(line): + line = line.split("#")[0] + line = line.split(";")[0] + return line + + +class ConfigFile(ConfigDict): + """A Git configuration file, like .git/config or ~/.gitconfig. + """ + + @classmethod + def from_file(cls, f): + """Read configuration from a file-like object.""" + ret = cls() + section = None + setting = None + for lineno, line in enumerate(f.readlines()): + line = line.lstrip() + if setting is None: + if _strip_comments(line).strip() == "": + continue + if line[0] == "[": + line = _strip_comments(line).rstrip() + if line[-1] != "]": + raise ValueError("expected trailing ]") + key = line.strip() + pts = key[1:-1].split(" ", 1) + pts[0] = pts[0].lower() + if len(pts) == 2: + if pts[1][0] != "\"" or pts[1][-1] != "\"": + raise ValueError( + "Invalid subsection " + pts[1]) + else: + pts[1] = pts[1][1:-1] + if not _check_section_name(pts[0]): + raise ValueError("invalid section name %s" % + pts[0]) + section = (pts[0], pts[1]) + else: + if not _check_section_name(pts[0]): + raise ValueError("invalid section name %s" % + pts[0]) + pts = pts[0].split(".", 1) + if len(pts) == 2: + section = (pts[0], pts[1]) + else: + section = (pts[0], ) + ret._values[section] = {} + else: + if section is None: + raise ValueError("setting %r without section" % line) + try: + setting, value = line.split("=", 1) + except ValueError: + setting = line + value = "true" + setting = setting.strip().lower() + if not _check_variable_name(setting): + raise ValueError("invalid variable name %s" % setting) + if value.endswith("\\\n"): + value = value[:-2] + continuation = True + else: + continuation = False + value = _parse_string(value) + ret._values[section][setting] = value + if not continuation: + setting = None + else: # continuation line + if line.endswith("\\\n"): + line = line[:-2] + continuation = True + else: + continuation = False + value = _parse_string(line) + ret._values[section][setting] += value + if not continuation: + setting = None + return ret + + @classmethod + def from_path(cls, path): + """Read configuration from a file on disk.""" + f = GitFile(path, 'rb') + try: + ret = cls.from_file(f) + ret.path = path + return ret + finally: + f.close() + + def write_to_path(self, path=None): + """Write configuration to a file on disk.""" + if path is None: + path = self.path + f = GitFile(path, 'wb') + try: + self.write_to_file(f) + finally: + f.close() + + def write_to_file(self, f): + """Write configuration to a file-like object.""" + for section, values in self._values.iteritems(): + try: + section_name, subsection_name = section + except ValueError: + (section_name, ) = section + subsection_name = None + if subsection_name is None: + f.write("[%s]\n" % section_name) + else: + f.write("[%s \"%s\"]\n" % (section_name, subsection_name)) + for key, value in values.iteritems(): + f.write("%s = %s\n" % (key, _escape_value(value))) + + +class StackedConfig(Config): + """Configuration which reads from multiple config files..""" + + def __init__(self, backends, writable=None): + self.backends = backends + self.writable = writable + + def __repr__(self): + return "<%s for %r>" % (self.__class__.__name__, self.backends) + + @classmethod + def default_backends(cls): + """Retrieve the default configuration. + + This will look in the repository configuration (if for_path is + specified), the users' home directory and the system + configuration. + """ + paths = [] + paths.append(os.path.expanduser("~/.gitconfig")) + paths.append("/etc/gitconfig") + backends = [] + for path in paths: + try: + cf = ConfigFile.from_path(path) + except (IOError, OSError), e: + if e.errno != errno.ENOENT: + raise + else: + continue + backends.append(cf) + return backends + + def get(self, section, name): + for backend in self.backends: + try: + return backend.get(section, name) + except KeyError: + pass + raise KeyError(name) + + def set(self, section, name, value): + if self.writable is None: + raise NotImplementedError(self.set) + return self.writable.set(section, name, value)