config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # config.py - Reading and writing Git config files
  2. # Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org>
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; version 2
  7. # of the License or (at your option) a later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  17. # MA 02110-1301, USA.
  18. """Reading and writing Git configuration files.
  19. TODO:
  20. * preserve formatting when updating configuration files
  21. * treat subsection names as case-insensitive for [branch.foo] style
  22. subsections
  23. """
  24. import errno
  25. import os
  26. import re
  27. from collections import OrderedDict
  28. from UserDict import DictMixin
  29. from dulwich.file import GitFile
  30. class Config(object):
  31. """A Git configuration."""
  32. def get(self, section, name):
  33. """Retrieve the contents of a configuration setting.
  34. :param section: Tuple with section name and optional subsection namee
  35. :param subsection: Subsection name
  36. :return: Contents of the setting
  37. :raise KeyError: if the value is not set
  38. """
  39. raise NotImplementedError(self.get)
  40. def get_boolean(self, section, name, default=None):
  41. """Retrieve a configuration setting as boolean.
  42. :param section: Tuple with section name and optional subsection namee
  43. :param name: Name of the setting, including section and possible
  44. subsection.
  45. :return: Contents of the setting
  46. :raise KeyError: if the value is not set
  47. """
  48. try:
  49. value = self.get(section, name)
  50. except KeyError:
  51. return default
  52. if value.lower() == "true":
  53. return True
  54. elif value.lower() == "false":
  55. return False
  56. raise ValueError("not a valid boolean string: %r" % value)
  57. def set(self, section, name, value):
  58. """Set a configuration value.
  59. :param name: Name of the configuration value, including section
  60. and optional subsection
  61. :param: Value of the setting
  62. """
  63. raise NotImplementedError(self.set)
  64. class ConfigDict(Config, DictMixin):
  65. """Git configuration stored in a dictionary."""
  66. def __init__(self, values=None):
  67. """Create a new ConfigDict."""
  68. if values is None:
  69. values = OrderedDict()
  70. self._values = values
  71. def __repr__(self):
  72. return "%s(%r)" % (self.__class__.__name__, self._values)
  73. def __eq__(self, other):
  74. return (
  75. isinstance(other, self.__class__) and
  76. other._values == self._values)
  77. def __getitem__(self, key):
  78. return self._values[key]
  79. def __setitem__(self, key, value):
  80. self._values[key] = value
  81. def keys(self):
  82. return self._values.keys()
  83. @classmethod
  84. def _parse_setting(cls, name):
  85. parts = name.split(".")
  86. if len(parts) == 3:
  87. return (parts[0], parts[1], parts[2])
  88. else:
  89. return (parts[0], None, parts[1])
  90. def get(self, section, name):
  91. if isinstance(section, basestring):
  92. section = (section, )
  93. if len(section) > 1:
  94. try:
  95. return self._values[section][name]
  96. except KeyError:
  97. pass
  98. return self._values[(section[0],)][name]
  99. def set(self, section, name, value):
  100. if isinstance(section, basestring):
  101. section = (section, )
  102. self._values.setdefault(section, OrderedDict())[name] = value
  103. def _format_string(value):
  104. if (value.startswith(" ") or
  105. value.startswith("\t") or
  106. value.endswith(" ") or
  107. value.endswith("\t")):
  108. return '"%s"' % _escape_value(value)
  109. return _escape_value(value)
  110. def _parse_string(value):
  111. value = value.strip()
  112. ret = []
  113. block = []
  114. in_quotes = False
  115. for c in value:
  116. if c == "\"":
  117. in_quotes = (not in_quotes)
  118. ret.append(_unescape_value("".join(block)))
  119. block = []
  120. elif c in ("#", ";") and not in_quotes:
  121. # the rest of the line is a comment
  122. break
  123. else:
  124. block.append(c)
  125. if in_quotes:
  126. raise ValueError("value starts with quote but lacks end quote")
  127. ret.append(_unescape_value("".join(block)).rstrip())
  128. return "".join(ret)
  129. def _unescape_value(value):
  130. """Unescape a value."""
  131. def unescape(c):
  132. return {
  133. "\\\\": "\\",
  134. "\\\"": "\"",
  135. "\\n": "\n",
  136. "\\t": "\t",
  137. "\\b": "\b",
  138. }[c.group(0)]
  139. return re.sub(r"(\\.)", unescape, value)
  140. def _escape_value(value):
  141. """Escape a value."""
  142. return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"")
  143. def _check_variable_name(name):
  144. for c in name:
  145. if not c.isalnum() and c != '-':
  146. return False
  147. return True
  148. def _check_section_name(name):
  149. for c in name:
  150. if not c.isalnum() and c not in ('-', '.'):
  151. return False
  152. return True
  153. def _strip_comments(line):
  154. line = line.split("#")[0]
  155. line = line.split(";")[0]
  156. return line
  157. class ConfigFile(ConfigDict):
  158. """A Git configuration file, like .git/config or ~/.gitconfig.
  159. """
  160. @classmethod
  161. def from_file(cls, f):
  162. """Read configuration from a file-like object."""
  163. ret = cls()
  164. section = None
  165. setting = None
  166. for lineno, line in enumerate(f.readlines()):
  167. line = line.lstrip()
  168. if setting is None:
  169. if len(line) > 0 and line[0] == "[":
  170. line = _strip_comments(line).rstrip()
  171. last = line.index("]")
  172. if last == -1:
  173. raise ValueError("expected trailing ]")
  174. pts = line[1:last].split(" ", 1)
  175. line = line[last+1:]
  176. pts[0] = pts[0].lower()
  177. if len(pts) == 2:
  178. if pts[1][0] != "\"" or pts[1][-1] != "\"":
  179. raise ValueError(
  180. "Invalid subsection " + pts[1])
  181. else:
  182. pts[1] = pts[1][1:-1]
  183. if not _check_section_name(pts[0]):
  184. raise ValueError("invalid section name %s" %
  185. pts[0])
  186. section = (pts[0], pts[1])
  187. else:
  188. if not _check_section_name(pts[0]):
  189. raise ValueError("invalid section name %s" %
  190. pts[0])
  191. pts = pts[0].split(".", 1)
  192. if len(pts) == 2:
  193. section = (pts[0], pts[1])
  194. else:
  195. section = (pts[0], )
  196. ret._values[section] = OrderedDict()
  197. if _strip_comments(line).strip() == "":
  198. continue
  199. if section is None:
  200. raise ValueError("setting %r without section" % line)
  201. try:
  202. setting, value = line.split("=", 1)
  203. except ValueError:
  204. setting = line
  205. value = "true"
  206. setting = setting.strip().lower()
  207. if not _check_variable_name(setting):
  208. raise ValueError("invalid variable name %s" % setting)
  209. if value.endswith("\\\n"):
  210. value = value[:-2]
  211. continuation = True
  212. else:
  213. continuation = False
  214. value = _parse_string(value)
  215. ret._values[section][setting] = value
  216. if not continuation:
  217. setting = None
  218. else: # continuation line
  219. if line.endswith("\\\n"):
  220. line = line[:-2]
  221. continuation = True
  222. else:
  223. continuation = False
  224. value = _parse_string(line)
  225. ret._values[section][setting] += value
  226. if not continuation:
  227. setting = None
  228. return ret
  229. @classmethod
  230. def from_path(cls, path):
  231. """Read configuration from a file on disk."""
  232. f = GitFile(path, 'rb')
  233. try:
  234. ret = cls.from_file(f)
  235. ret.path = path
  236. return ret
  237. finally:
  238. f.close()
  239. def write_to_path(self, path=None):
  240. """Write configuration to a file on disk."""
  241. if path is None:
  242. path = self.path
  243. f = GitFile(path, 'wb')
  244. try:
  245. self.write_to_file(f)
  246. finally:
  247. f.close()
  248. def write_to_file(self, f):
  249. """Write configuration to a file-like object."""
  250. for section, values in self._values.iteritems():
  251. try:
  252. section_name, subsection_name = section
  253. except ValueError:
  254. (section_name, ) = section
  255. subsection_name = None
  256. if subsection_name is None:
  257. f.write("[%s]\n" % section_name)
  258. else:
  259. f.write("[%s \"%s\"]\n" % (section_name, subsection_name))
  260. for key, value in values.iteritems():
  261. f.write("\t%s = %s\n" % (key, _escape_value(value)))
  262. class StackedConfig(Config):
  263. """Configuration which reads from multiple config files.."""
  264. def __init__(self, backends, writable=None):
  265. self.backends = backends
  266. self.writable = writable
  267. def __repr__(self):
  268. return "<%s for %r>" % (self.__class__.__name__, self.backends)
  269. @classmethod
  270. def default_backends(cls):
  271. """Retrieve the default configuration.
  272. This will look in the repository configuration (if for_path is
  273. specified), the users' home directory and the system
  274. configuration.
  275. """
  276. paths = []
  277. paths.append(os.path.expanduser("~/.gitconfig"))
  278. paths.append("/etc/gitconfig")
  279. backends = []
  280. for path in paths:
  281. try:
  282. cf = ConfigFile.from_path(path)
  283. except (IOError, OSError), e:
  284. if e.errno != errno.ENOENT:
  285. raise
  286. else:
  287. continue
  288. backends.append(cf)
  289. return backends
  290. def get(self, section, name):
  291. for backend in self.backends:
  292. try:
  293. return backend.get(section, name)
  294. except KeyError:
  295. pass
  296. raise KeyError(name)
  297. def set(self, section, name, value):
  298. if self.writable is None:
  299. raise NotImplementedError(self.set)
  300. return self.writable.set(section, name, value)