##// END OF EJS Templates
follow-up on texts missing from 'users groups'/'repositories group' cleanup
follow-up on texts missing from 'users groups'/'repositories group' cleanup

File last commit:

r2007:324ac367 beta
r3416:5706f6ab beta
Show More
config.py
347 lines | 10.9 KiB | text/x-python | PythonLexer
# config.py - Reading and writing Git config files
# Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org>
#
# 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)