config.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. # config.py - Reading and writing Git config files
  2. # Copyright (C) 2011-2013 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 section: Tuple with section name and optional subsection namee
  60. :param name: Name of the configuration value, including section
  61. and optional subsection
  62. :param: Value of the setting
  63. """
  64. raise NotImplementedError(self.set)
  65. def iteritems(self, section):
  66. """Iterate over the configuration pairs for a specific section.
  67. :param section: Tuple with section name and optional subsection namee
  68. :return: Iterator over (name, value) pairs
  69. """
  70. raise NotImplementedError(self.iteritems)
  71. def itersections(self):
  72. """Iterate over the sections.
  73. :return: Iterator over section tuples
  74. """
  75. raise NotImplementedError(self.itersections)
  76. class ConfigDict(Config, DictMixin):
  77. """Git configuration stored in a dictionary."""
  78. def __init__(self, values=None):
  79. """Create a new ConfigDict."""
  80. if values is None:
  81. values = OrderedDict()
  82. self._values = values
  83. def __repr__(self):
  84. return "%s(%r)" % (self.__class__.__name__, self._values)
  85. def __eq__(self, other):
  86. return (
  87. isinstance(other, self.__class__) and
  88. other._values == self._values)
  89. def __getitem__(self, key):
  90. return self._values[key]
  91. def __setitem__(self, key, value):
  92. self._values[key] = value
  93. def keys(self):
  94. return self._values.keys()
  95. @classmethod
  96. def _parse_setting(cls, name):
  97. parts = name.split(".")
  98. if len(parts) == 3:
  99. return (parts[0], parts[1], parts[2])
  100. else:
  101. return (parts[0], None, parts[1])
  102. def get(self, section, name):
  103. if isinstance(section, basestring):
  104. section = (section, )
  105. if len(section) > 1:
  106. try:
  107. return self._values[section][name]
  108. except KeyError:
  109. pass
  110. return self._values[(section[0],)][name]
  111. def set(self, section, name, value):
  112. if isinstance(section, basestring):
  113. section = (section, )
  114. self._values.setdefault(section, OrderedDict())[name] = value
  115. def iteritems(self, section):
  116. return self._values.get(section, OrderedDict()).iteritems()
  117. def itersections(self):
  118. return self._values.keys()
  119. def _format_string(value):
  120. if (value.startswith(" ") or
  121. value.startswith("\t") or
  122. value.endswith(" ") or
  123. value.endswith("\t")):
  124. return '"%s"' % _escape_value(value)
  125. return _escape_value(value)
  126. def _parse_string(value):
  127. value = value.strip()
  128. ret = []
  129. block = []
  130. in_quotes = False
  131. for c in value:
  132. if c == "\"":
  133. in_quotes = (not in_quotes)
  134. ret.append(_unescape_value("".join(block)))
  135. block = []
  136. elif c in ("#", ";") and not in_quotes:
  137. # the rest of the line is a comment
  138. break
  139. else:
  140. block.append(c)
  141. if in_quotes:
  142. raise ValueError("value starts with quote but lacks end quote")
  143. ret.append(_unescape_value("".join(block)).rstrip())
  144. return "".join(ret)
  145. def _unescape_value(value):
  146. """Unescape a value."""
  147. def unescape(c):
  148. return {
  149. "\\\\": "\\",
  150. "\\\"": "\"",
  151. "\\n": "\n",
  152. "\\t": "\t",
  153. "\\b": "\b",
  154. }[c.group(0)]
  155. return re.sub(r"(\\.)", unescape, value)
  156. def _escape_value(value):
  157. """Escape a value."""
  158. return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"")
  159. def _check_variable_name(name):
  160. for c in name:
  161. if not c.isalnum() and c != '-':
  162. return False
  163. return True
  164. def _check_section_name(name):
  165. for c in name:
  166. if not c.isalnum() and c not in ('-', '.'):
  167. return False
  168. return True
  169. def _strip_comments(line):
  170. line = line.split("#")[0]
  171. line = line.split(";")[0]
  172. return line
  173. class ConfigFile(ConfigDict):
  174. """A Git configuration file, like .git/config or ~/.gitconfig.
  175. """
  176. @classmethod
  177. def from_file(cls, f):
  178. """Read configuration from a file-like object."""
  179. ret = cls()
  180. section = None
  181. setting = None
  182. for lineno, line in enumerate(f.readlines()):
  183. line = line.lstrip()
  184. if setting is None:
  185. if len(line) > 0 and line[0] == "[":
  186. line = _strip_comments(line).rstrip()
  187. last = line.index("]")
  188. if last == -1:
  189. raise ValueError("expected trailing ]")
  190. pts = line[1:last].split(" ", 1)
  191. line = line[last+1:]
  192. pts[0] = pts[0].lower()
  193. if len(pts) == 2:
  194. if pts[1][0] != "\"" or pts[1][-1] != "\"":
  195. raise ValueError(
  196. "Invalid subsection " + pts[1])
  197. else:
  198. pts[1] = pts[1][1:-1]
  199. if not _check_section_name(pts[0]):
  200. raise ValueError("invalid section name %s" %
  201. pts[0])
  202. section = (pts[0], pts[1])
  203. else:
  204. if not _check_section_name(pts[0]):
  205. raise ValueError("invalid section name %s" %
  206. pts[0])
  207. pts = pts[0].split(".", 1)
  208. if len(pts) == 2:
  209. section = (pts[0], pts[1])
  210. else:
  211. section = (pts[0], )
  212. ret._values[section] = OrderedDict()
  213. if _strip_comments(line).strip() == "":
  214. continue
  215. if section is None:
  216. raise ValueError("setting %r without section" % line)
  217. try:
  218. setting, value = line.split("=", 1)
  219. except ValueError:
  220. setting = line
  221. value = "true"
  222. setting = setting.strip().lower()
  223. if not _check_variable_name(setting):
  224. raise ValueError("invalid variable name %s" % setting)
  225. if value.endswith("\\\n"):
  226. value = value[:-2]
  227. continuation = True
  228. else:
  229. continuation = False
  230. value = _parse_string(value)
  231. ret._values[section][setting] = value
  232. if not continuation:
  233. setting = None
  234. else: # continuation line
  235. if line.endswith("\\\n"):
  236. line = line[:-2]
  237. continuation = True
  238. else:
  239. continuation = False
  240. value = _parse_string(line)
  241. ret._values[section][setting] += value
  242. if not continuation:
  243. setting = None
  244. return ret
  245. @classmethod
  246. def from_path(cls, path):
  247. """Read configuration from a file on disk."""
  248. f = GitFile(path, 'rb')
  249. try:
  250. ret = cls.from_file(f)
  251. ret.path = path
  252. return ret
  253. finally:
  254. f.close()
  255. def write_to_path(self, path=None):
  256. """Write configuration to a file on disk."""
  257. if path is None:
  258. path = self.path
  259. f = GitFile(path, 'wb')
  260. try:
  261. self.write_to_file(f)
  262. finally:
  263. f.close()
  264. def write_to_file(self, f):
  265. """Write configuration to a file-like object."""
  266. for section, values in self._values.iteritems():
  267. try:
  268. section_name, subsection_name = section
  269. except ValueError:
  270. (section_name, ) = section
  271. subsection_name = None
  272. if subsection_name is None:
  273. f.write("[%s]\n" % section_name)
  274. else:
  275. f.write("[%s \"%s\"]\n" % (section_name, subsection_name))
  276. for key, value in values.iteritems():
  277. f.write("\t%s = %s\n" % (key, _escape_value(value)))
  278. class StackedConfig(Config):
  279. """Configuration which reads from multiple config files.."""
  280. def __init__(self, backends, writable=None):
  281. self.backends = backends
  282. self.writable = writable
  283. def __repr__(self):
  284. return "<%s for %r>" % (self.__class__.__name__, self.backends)
  285. @classmethod
  286. def default_backends(cls):
  287. """Retrieve the default configuration.
  288. This will look in the users' home directory and the system
  289. configuration.
  290. """
  291. paths = []
  292. paths.append(os.path.expanduser("~/.gitconfig"))
  293. paths.append("/etc/gitconfig")
  294. backends = []
  295. for path in paths:
  296. try:
  297. cf = ConfigFile.from_path(path)
  298. except (IOError, OSError) as e:
  299. if e.errno != errno.ENOENT:
  300. raise
  301. else:
  302. continue
  303. backends.append(cf)
  304. return backends
  305. def get(self, section, name):
  306. for backend in self.backends:
  307. try:
  308. return backend.get(section, name)
  309. except KeyError:
  310. pass
  311. raise KeyError(name)
  312. def set(self, section, name, value):
  313. if self.writable is None:
  314. raise NotImplementedError(self.set)
  315. return self.writable.set(section, name, value)