config.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. # config.py - Reading and writing Git config files
  2. # Copyright (C) 2011-2013 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Reading and writing Git configuration files.
  21. TODO:
  22. * preserve formatting when updating configuration files
  23. * treat subsection names as case-insensitive for [branch.foo] style
  24. subsections
  25. """
  26. import os
  27. import sys
  28. from typing import BinaryIO, Tuple, Optional
  29. from collections import (
  30. OrderedDict,
  31. )
  32. try:
  33. from collections.abc import (
  34. Iterable,
  35. MutableMapping,
  36. )
  37. except ImportError: # python < 3.7
  38. from collections import (
  39. Iterable,
  40. MutableMapping,
  41. )
  42. from dulwich.file import GitFile
  43. SENTINAL = object()
  44. def lower_key(key):
  45. if isinstance(key, (bytes, str)):
  46. return key.lower()
  47. if isinstance(key, Iterable):
  48. return type(key)(map(lower_key, key))
  49. return key
  50. class CaseInsensitiveDict(OrderedDict):
  51. @classmethod
  52. def make(cls, dict_in=None):
  53. if isinstance(dict_in, cls):
  54. return dict_in
  55. out = cls()
  56. if dict_in is None:
  57. return out
  58. if not isinstance(dict_in, MutableMapping):
  59. raise TypeError
  60. for key, value in dict_in.items():
  61. out[key] = value
  62. return out
  63. def __setitem__(self, key, value, **kwargs):
  64. key = lower_key(key)
  65. super(CaseInsensitiveDict, self).__setitem__(key, value, **kwargs)
  66. def __getitem__(self, item):
  67. key = lower_key(item)
  68. return super(CaseInsensitiveDict, self).__getitem__(key)
  69. def get(self, key, default=SENTINAL):
  70. try:
  71. return self[key]
  72. except KeyError:
  73. pass
  74. if default is SENTINAL:
  75. return type(self)()
  76. return default
  77. def setdefault(self, key, default=SENTINAL):
  78. try:
  79. return self[key]
  80. except KeyError:
  81. self[key] = self.get(key, default)
  82. return self[key]
  83. class Config(object):
  84. """A Git configuration."""
  85. def get(self, section, name):
  86. """Retrieve the contents of a configuration setting.
  87. Args:
  88. section: Tuple with section name and optional subsection namee
  89. subsection: Subsection name
  90. Returns:
  91. Contents of the setting
  92. Raises:
  93. KeyError: if the value is not set
  94. """
  95. raise NotImplementedError(self.get)
  96. def get_boolean(self, section, name, default=None):
  97. """Retrieve a configuration setting as boolean.
  98. Args:
  99. section: Tuple with section name and optional subsection name
  100. name: Name of the setting, including section and possible
  101. subsection.
  102. Returns:
  103. Contents of the setting
  104. Raises:
  105. KeyError: if the value is not set
  106. """
  107. try:
  108. value = self.get(section, name)
  109. except KeyError:
  110. return default
  111. if value.lower() == b"true":
  112. return True
  113. elif value.lower() == b"false":
  114. return False
  115. raise ValueError("not a valid boolean string: %r" % value)
  116. def set(self, section, name, value):
  117. """Set a configuration value.
  118. Args:
  119. section: Tuple with section name and optional subsection namee
  120. name: Name of the configuration value, including section
  121. and optional subsection
  122. value: value of the setting
  123. """
  124. raise NotImplementedError(self.set)
  125. def iteritems(self, section):
  126. """Iterate over the configuration pairs for a specific section.
  127. Args:
  128. section: Tuple with section name and optional subsection namee
  129. Returns:
  130. Iterator over (name, value) pairs
  131. """
  132. raise NotImplementedError(self.iteritems)
  133. def itersections(self):
  134. """Iterate over the sections.
  135. Returns: Iterator over section tuples
  136. """
  137. raise NotImplementedError(self.itersections)
  138. def has_section(self, name):
  139. """Check if a specified section exists.
  140. Args:
  141. name: Name of section to check for
  142. Returns:
  143. boolean indicating whether the section exists
  144. """
  145. return name in self.itersections()
  146. class ConfigDict(Config, MutableMapping):
  147. """Git configuration stored in a dictionary."""
  148. def __init__(self, values=None, encoding=None):
  149. """Create a new ConfigDict."""
  150. if encoding is None:
  151. encoding = sys.getdefaultencoding()
  152. self.encoding = encoding
  153. self._values = CaseInsensitiveDict.make(values)
  154. def __repr__(self):
  155. return "%s(%r)" % (self.__class__.__name__, self._values)
  156. def __eq__(self, other):
  157. return isinstance(other, self.__class__) and other._values == self._values
  158. def __getitem__(self, key):
  159. return self._values.__getitem__(key)
  160. def __setitem__(self, key, value):
  161. return self._values.__setitem__(key, value)
  162. def __delitem__(self, key):
  163. return self._values.__delitem__(key)
  164. def __iter__(self):
  165. return self._values.__iter__()
  166. def __len__(self):
  167. return self._values.__len__()
  168. @classmethod
  169. def _parse_setting(cls, name):
  170. parts = name.split(".")
  171. if len(parts) == 3:
  172. return (parts[0], parts[1], parts[2])
  173. else:
  174. return (parts[0], None, parts[1])
  175. def _check_section_and_name(self, section, name):
  176. if not isinstance(section, tuple):
  177. section = (section,)
  178. section = tuple(
  179. [
  180. subsection.encode(self.encoding)
  181. if not isinstance(subsection, bytes)
  182. else subsection
  183. for subsection in section
  184. ]
  185. )
  186. if not isinstance(name, bytes):
  187. name = name.encode(self.encoding)
  188. return section, name
  189. def get(self, section, name):
  190. section, name = self._check_section_and_name(section, name)
  191. if len(section) > 1:
  192. try:
  193. return self._values[section][name]
  194. except KeyError:
  195. pass
  196. return self._values[(section[0],)][name]
  197. def set(self, section, name, value):
  198. section, name = self._check_section_and_name(section, name)
  199. if type(value) not in (bool, bytes):
  200. value = value.encode(self.encoding)
  201. self._values.setdefault(section)[name] = value
  202. def iteritems(self, section):
  203. return self._values.get(section).items()
  204. def itersections(self):
  205. return self._values.keys()
  206. def _format_string(value):
  207. if (
  208. value.startswith(b" ")
  209. or value.startswith(b"\t")
  210. or value.endswith(b" ")
  211. or b"#" in value
  212. or value.endswith(b"\t")
  213. ):
  214. return b'"' + _escape_value(value) + b'"'
  215. else:
  216. return _escape_value(value)
  217. _ESCAPE_TABLE = {
  218. ord(b"\\"): ord(b"\\"),
  219. ord(b'"'): ord(b'"'),
  220. ord(b"n"): ord(b"\n"),
  221. ord(b"t"): ord(b"\t"),
  222. ord(b"b"): ord(b"\b"),
  223. }
  224. _COMMENT_CHARS = [ord(b"#"), ord(b";")]
  225. _WHITESPACE_CHARS = [ord(b"\t"), ord(b" ")]
  226. def _parse_string(value):
  227. value = bytearray(value.strip())
  228. ret = bytearray()
  229. whitespace = bytearray()
  230. in_quotes = False
  231. i = 0
  232. while i < len(value):
  233. c = value[i]
  234. if c == ord(b"\\"):
  235. i += 1
  236. try:
  237. v = _ESCAPE_TABLE[value[i]]
  238. except IndexError:
  239. raise ValueError(
  240. "escape character in %r at %d before end of string" % (value, i)
  241. )
  242. except KeyError:
  243. raise ValueError(
  244. "escape character followed by unknown character "
  245. "%s at %d in %r" % (value[i], i, value)
  246. )
  247. if whitespace:
  248. ret.extend(whitespace)
  249. whitespace = bytearray()
  250. ret.append(v)
  251. elif c == ord(b'"'):
  252. in_quotes = not in_quotes
  253. elif c in _COMMENT_CHARS and not in_quotes:
  254. # the rest of the line is a comment
  255. break
  256. elif c in _WHITESPACE_CHARS:
  257. whitespace.append(c)
  258. else:
  259. if whitespace:
  260. ret.extend(whitespace)
  261. whitespace = bytearray()
  262. ret.append(c)
  263. i += 1
  264. if in_quotes:
  265. raise ValueError("missing end quote")
  266. return bytes(ret)
  267. def _escape_value(value):
  268. """Escape a value."""
  269. value = value.replace(b"\\", b"\\\\")
  270. value = value.replace(b"\n", b"\\n")
  271. value = value.replace(b"\t", b"\\t")
  272. value = value.replace(b'"', b'\\"')
  273. return value
  274. def _check_variable_name(name):
  275. for i in range(len(name)):
  276. c = name[i : i + 1]
  277. if not c.isalnum() and c != b"-":
  278. return False
  279. return True
  280. def _check_section_name(name):
  281. for i in range(len(name)):
  282. c = name[i : i + 1]
  283. if not c.isalnum() and c not in (b"-", b"."):
  284. return False
  285. return True
  286. def _strip_comments(line):
  287. comment_bytes = {ord(b"#"), ord(b";")}
  288. quote = ord(b'"')
  289. string_open = False
  290. # Normalize line to bytearray for simple 2/3 compatibility
  291. for i, character in enumerate(bytearray(line)):
  292. # Comment characters outside balanced quotes denote comment start
  293. if character == quote:
  294. string_open = not string_open
  295. elif not string_open and character in comment_bytes:
  296. return line[:i]
  297. return line
  298. class ConfigFile(ConfigDict):
  299. """A Git configuration file, like .git/config or ~/.gitconfig."""
  300. def __init__(self, values=None, encoding=None):
  301. super(ConfigFile, self).__init__(values=values, encoding=encoding)
  302. self.path = None
  303. @classmethod
  304. def from_file(cls, f: BinaryIO) -> "ConfigFile":
  305. """Read configuration from a file-like object."""
  306. ret = cls()
  307. section = None # type: Optional[Tuple[bytes, ...]]
  308. setting = None
  309. continuation = None
  310. for lineno, line in enumerate(f.readlines()):
  311. line = line.lstrip()
  312. if setting is None:
  313. # Parse section header ("[bla]")
  314. if len(line) > 0 and line[:1] == b"[":
  315. line = _strip_comments(line).rstrip()
  316. try:
  317. last = line.index(b"]")
  318. except ValueError:
  319. raise ValueError("expected trailing ]")
  320. pts = line[1:last].split(b" ", 1)
  321. line = line[last + 1 :]
  322. if len(pts) == 2:
  323. if pts[1][:1] != b'"' or pts[1][-1:] != b'"':
  324. raise ValueError("Invalid subsection %r" % pts[1])
  325. else:
  326. pts[1] = pts[1][1:-1]
  327. if not _check_section_name(pts[0]):
  328. raise ValueError("invalid section name %r" % pts[0])
  329. section = (pts[0], pts[1])
  330. else:
  331. if not _check_section_name(pts[0]):
  332. raise ValueError("invalid section name %r" % pts[0])
  333. pts = pts[0].split(b".", 1)
  334. if len(pts) == 2:
  335. section = (pts[0], pts[1])
  336. else:
  337. section = (pts[0],)
  338. ret._values.setdefault(section)
  339. if _strip_comments(line).strip() == b"":
  340. continue
  341. if section is None:
  342. raise ValueError("setting %r without section" % line)
  343. try:
  344. setting, value = line.split(b"=", 1)
  345. except ValueError:
  346. setting = line
  347. value = b"true"
  348. setting = setting.strip()
  349. if not _check_variable_name(setting):
  350. raise ValueError("invalid variable name %r" % setting)
  351. if value.endswith(b"\\\n"):
  352. continuation = value[:-2]
  353. else:
  354. continuation = None
  355. value = _parse_string(value)
  356. ret._values[section][setting] = value
  357. setting = None
  358. else: # continuation line
  359. if line.endswith(b"\\\n"):
  360. continuation += line[:-2]
  361. else:
  362. continuation += line
  363. value = _parse_string(continuation)
  364. ret._values[section][setting] = value
  365. continuation = None
  366. setting = None
  367. return ret
  368. @classmethod
  369. def from_path(cls, path) -> "ConfigFile":
  370. """Read configuration from a file on disk."""
  371. with GitFile(path, "rb") as f:
  372. ret = cls.from_file(f)
  373. ret.path = path
  374. return ret
  375. def write_to_path(self, path=None) -> None:
  376. """Write configuration to a file on disk."""
  377. if path is None:
  378. path = self.path
  379. with GitFile(path, "wb") as f:
  380. self.write_to_file(f)
  381. def write_to_file(self, f: BinaryIO) -> None:
  382. """Write configuration to a file-like object."""
  383. for section, values in self._values.items():
  384. try:
  385. section_name, subsection_name = section
  386. except ValueError:
  387. (section_name,) = section
  388. subsection_name = None
  389. if subsection_name is None:
  390. f.write(b"[" + section_name + b"]\n")
  391. else:
  392. f.write(b"[" + section_name + b' "' + subsection_name + b'"]\n')
  393. for key, value in values.items():
  394. if value is True:
  395. value = b"true"
  396. elif value is False:
  397. value = b"false"
  398. else:
  399. value = _format_string(value)
  400. f.write(b"\t" + key + b" = " + value + b"\n")
  401. def get_xdg_config_home_path(*path_segments):
  402. xdg_config_home = os.environ.get(
  403. "XDG_CONFIG_HOME",
  404. os.path.expanduser("~/.config/"),
  405. )
  406. return os.path.join(xdg_config_home, *path_segments)
  407. def _find_git_in_win_path():
  408. for exe in ("git.exe", "git.cmd"):
  409. for path in os.environ.get("PATH", "").split(";"):
  410. if os.path.exists(os.path.join(path, exe)):
  411. # exe path is .../Git/bin/git.exe or .../Git/cmd/git.exe
  412. git_dir, _bin_dir = os.path.split(path)
  413. yield git_dir
  414. break
  415. def _find_git_in_win_reg():
  416. import platform
  417. import winreg
  418. if platform.machine() == "AMD64":
  419. subkey = (
  420. "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\"
  421. "CurrentVersion\\Uninstall\\Git_is1"
  422. )
  423. else:
  424. subkey = (
  425. "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
  426. "Uninstall\\Git_is1"
  427. )
  428. for key in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
  429. try:
  430. with winreg.OpenKey(key, subkey) as k:
  431. val, typ = winreg.QueryValueEx(k, "InstallLocation")
  432. if typ == winreg.REG_SZ:
  433. yield val
  434. except OSError:
  435. pass
  436. # There is no set standard for system config dirs on windows. We try the
  437. # following:
  438. # - %PROGRAMDATA%/Git/config - (deprecated) Windows config dir per CGit docs
  439. # - %PROGRAMFILES%/Git/etc/gitconfig - Git for Windows (msysgit) config dir
  440. # Used if CGit installation (Git/bin/git.exe) is found in PATH in the
  441. # system registry
  442. def get_win_system_paths():
  443. if "PROGRAMDATA" in os.environ:
  444. yield os.path.join(os.environ["PROGRAMDATA"], "Git", "config")
  445. for git_dir in _find_git_in_win_path():
  446. yield os.path.join(git_dir, "etc", "gitconfig")
  447. for git_dir in _find_git_in_win_reg():
  448. yield os.path.join(git_dir, "etc", "gitconfig")
  449. class StackedConfig(Config):
  450. """Configuration which reads from multiple config files.."""
  451. def __init__(self, backends, writable=None):
  452. self.backends = backends
  453. self.writable = writable
  454. def __repr__(self):
  455. return "<%s for %r>" % (self.__class__.__name__, self.backends)
  456. @classmethod
  457. def default(cls):
  458. return cls(cls.default_backends())
  459. @classmethod
  460. def default_backends(cls):
  461. """Retrieve the default configuration.
  462. See git-config(1) for details on the files searched.
  463. """
  464. paths = []
  465. paths.append(os.path.expanduser("~/.gitconfig"))
  466. paths.append(get_xdg_config_home_path("git", "config"))
  467. if "GIT_CONFIG_NOSYSTEM" not in os.environ:
  468. paths.append("/etc/gitconfig")
  469. if sys.platform == "win32":
  470. paths.extend(get_win_system_paths())
  471. backends = []
  472. for path in paths:
  473. try:
  474. cf = ConfigFile.from_path(path)
  475. except FileNotFoundError:
  476. continue
  477. backends.append(cf)
  478. return backends
  479. def get(self, section, name):
  480. if not isinstance(section, tuple):
  481. section = (section,)
  482. for backend in self.backends:
  483. try:
  484. return backend.get(section, name)
  485. except KeyError:
  486. pass
  487. raise KeyError(name)
  488. def set(self, section, name, value):
  489. if self.writable is None:
  490. raise NotImplementedError(self.set)
  491. return self.writable.set(section, name, value)
  492. def parse_submodules(config):
  493. """Parse a gitmodules GitConfig file, returning submodules.
  494. Args:
  495. config: A `ConfigFile`
  496. Returns:
  497. list of tuples (submodule path, url, name),
  498. where name is quoted part of the section's name.
  499. """
  500. for section in config.keys():
  501. section_kind, section_name = section
  502. if section_kind == b"submodule":
  503. sm_path = config.get(section, b"path")
  504. sm_url = config.get(section, b"url")
  505. yield (sm_path, sm_url, section_name)