config.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  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. import warnings
  29. from typing import BinaryIO, Tuple, Optional
  30. from collections import (
  31. OrderedDict,
  32. )
  33. try:
  34. from collections.abc import (
  35. Iterable,
  36. MutableMapping,
  37. )
  38. except ImportError: # python < 3.7
  39. from collections import ( # type: ignore
  40. Iterable,
  41. MutableMapping,
  42. )
  43. from dulwich.file import GitFile
  44. SENTINAL = object()
  45. def lower_key(key):
  46. if isinstance(key, (bytes, str)):
  47. return key.lower()
  48. if isinstance(key, Iterable):
  49. return type(key)(map(lower_key, key))
  50. return key
  51. class CaseInsensitiveOrderedMultiDict(MutableMapping):
  52. def __init__(self):
  53. self._real = []
  54. self._keyed = {}
  55. @classmethod
  56. def make(cls, dict_in=None):
  57. if isinstance(dict_in, cls):
  58. return dict_in
  59. out = cls()
  60. if dict_in is None:
  61. return out
  62. if not isinstance(dict_in, MutableMapping):
  63. raise TypeError
  64. for key, value in dict_in.items():
  65. out[key] = value
  66. return out
  67. def __len__(self):
  68. return len(self._keyed)
  69. def keys(self):
  70. return self._keyed.keys()
  71. def items(self):
  72. return iter(self._real)
  73. def __iter__(self):
  74. return self._keyed.__iter__()
  75. def values(self):
  76. return self._keyed.values()
  77. def __setitem__(self, key, value):
  78. self._real.append((key, value))
  79. self._keyed[lower_key(key)] = value
  80. def __delitem__(self, key):
  81. key = lower_key(key)
  82. del self._keyed[key]
  83. for i, (actual, unused_value) in reversed(enumerate(self._real)):
  84. if lower_key(actual) == key:
  85. del self._real[i]
  86. def __getitem__(self, item):
  87. return self._keyed[lower_key(item)]
  88. def get(self, key, default=SENTINAL):
  89. try:
  90. return self[key]
  91. except KeyError:
  92. pass
  93. if default is SENTINAL:
  94. return type(self)()
  95. return default
  96. def get_all(self, key):
  97. key = lower_key(key)
  98. for actual, value in self._real:
  99. if lower_key(actual) == key:
  100. yield value
  101. def setdefault(self, key, default=SENTINAL):
  102. try:
  103. return self[key]
  104. except KeyError:
  105. self[key] = self.get(key, default)
  106. return self[key]
  107. class Config(object):
  108. """A Git configuration."""
  109. def get(self, section, name):
  110. """Retrieve the contents of a configuration setting.
  111. Args:
  112. section: Tuple with section name and optional subsection namee
  113. name: Variable name
  114. Returns:
  115. Contents of the setting
  116. Raises:
  117. KeyError: if the value is not set
  118. """
  119. raise NotImplementedError(self.get)
  120. def get_multivar(self, section, name):
  121. """Retrieve the contents of a multivar configuration setting.
  122. Args:
  123. section: Tuple with section name and optional subsection namee
  124. name: Variable name
  125. Returns:
  126. Contents of the setting as iterable
  127. Raises:
  128. KeyError: if the value is not set
  129. """
  130. raise NotImplementedError(self.get_multivar)
  131. def get_boolean(self, section, name, default=None):
  132. """Retrieve a configuration setting as boolean.
  133. Args:
  134. section: Tuple with section name and optional subsection name
  135. name: Name of the setting, including section and possible
  136. subsection.
  137. Returns:
  138. Contents of the setting
  139. Raises:
  140. KeyError: if the value is not set
  141. """
  142. try:
  143. value = self.get(section, name)
  144. except KeyError:
  145. return default
  146. if value.lower() == b"true":
  147. return True
  148. elif value.lower() == b"false":
  149. return False
  150. raise ValueError("not a valid boolean string: %r" % value)
  151. def set(self, section, name, value):
  152. """Set a configuration value.
  153. Args:
  154. section: Tuple with section name and optional subsection namee
  155. name: Name of the configuration value, including section
  156. and optional subsection
  157. value: value of the setting
  158. """
  159. raise NotImplementedError(self.set)
  160. def items(self, section):
  161. """Iterate over the configuration pairs for a specific section.
  162. Args:
  163. section: Tuple with section name and optional subsection namee
  164. Returns:
  165. Iterator over (name, value) pairs
  166. """
  167. raise NotImplementedError(self.items)
  168. def iteritems(self, section):
  169. """Iterate over the configuration pairs for a specific section.
  170. Args:
  171. section: Tuple with section name and optional subsection namee
  172. Returns:
  173. Iterator over (name, value) pairs
  174. """
  175. warnings.warn(
  176. "Use %s.items instead." % type(self).__name__,
  177. DeprecationWarning,
  178. stacklevel=3,
  179. )
  180. return self.items(section)
  181. def itersections(self):
  182. warnings.warn(
  183. "Use %s.items instead." % type(self).__name__,
  184. DeprecationWarning,
  185. stacklevel=3,
  186. )
  187. return self.sections()
  188. def sections(self):
  189. """Iterate over the sections.
  190. Returns: Iterator over section tuples
  191. """
  192. raise NotImplementedError(self.sections)
  193. def has_section(self, name):
  194. """Check if a specified section exists.
  195. Args:
  196. name: Name of section to check for
  197. Returns:
  198. boolean indicating whether the section exists
  199. """
  200. return name in self.sections()
  201. class ConfigDict(Config, MutableMapping):
  202. """Git configuration stored in a dictionary."""
  203. def __init__(self, values=None, encoding=None):
  204. """Create a new ConfigDict."""
  205. if encoding is None:
  206. encoding = sys.getdefaultencoding()
  207. self.encoding = encoding
  208. self._values = CaseInsensitiveOrderedMultiDict.make(values)
  209. def __repr__(self):
  210. return "%s(%r)" % (self.__class__.__name__, self._values)
  211. def __eq__(self, other):
  212. return isinstance(other, self.__class__) and other._values == self._values
  213. def __getitem__(self, key):
  214. return self._values.__getitem__(key)
  215. def __setitem__(self, key, value):
  216. return self._values.__setitem__(key, value)
  217. def __delitem__(self, key):
  218. return self._values.__delitem__(key)
  219. def __iter__(self):
  220. return self._values.__iter__()
  221. def __len__(self):
  222. return self._values.__len__()
  223. @classmethod
  224. def _parse_setting(cls, name):
  225. parts = name.split(".")
  226. if len(parts) == 3:
  227. return (parts[0], parts[1], parts[2])
  228. else:
  229. return (parts[0], None, parts[1])
  230. def _check_section_and_name(self, section, name):
  231. if not isinstance(section, tuple):
  232. section = (section,)
  233. section = tuple(
  234. [
  235. subsection.encode(self.encoding)
  236. if not isinstance(subsection, bytes)
  237. else subsection
  238. for subsection in section
  239. ]
  240. )
  241. if not isinstance(name, bytes):
  242. name = name.encode(self.encoding)
  243. return section, name
  244. def get_multivar(self, section, name):
  245. section, name = self._check_section_and_name(section, name)
  246. if len(section) > 1:
  247. try:
  248. return self._values[section][name]
  249. except KeyError:
  250. pass
  251. return self._values[(section[0],)].get_all(name)
  252. def get(self, section, name):
  253. section, name = self._check_section_and_name(section, name)
  254. if len(section) > 1:
  255. try:
  256. return self._values[section][name]
  257. except KeyError:
  258. pass
  259. return self._values[(section[0],)][name]
  260. def set(self, section, name, value):
  261. section, name = self._check_section_and_name(section, name)
  262. if type(value) not in (bool, bytes):
  263. value = value.encode(self.encoding)
  264. self._values.setdefault(section)[name] = value
  265. def items(self, section):
  266. return self._values.get(section).items()
  267. def sections(self):
  268. return self._values.keys()
  269. def _format_string(value):
  270. if (
  271. value.startswith(b" ")
  272. or value.startswith(b"\t")
  273. or value.endswith(b" ")
  274. or b"#" in value
  275. or value.endswith(b"\t")
  276. ):
  277. return b'"' + _escape_value(value) + b'"'
  278. else:
  279. return _escape_value(value)
  280. _ESCAPE_TABLE = {
  281. ord(b"\\"): ord(b"\\"),
  282. ord(b'"'): ord(b'"'),
  283. ord(b"n"): ord(b"\n"),
  284. ord(b"t"): ord(b"\t"),
  285. ord(b"b"): ord(b"\b"),
  286. }
  287. _COMMENT_CHARS = [ord(b"#"), ord(b";")]
  288. _WHITESPACE_CHARS = [ord(b"\t"), ord(b" ")]
  289. def _parse_string(value):
  290. value = bytearray(value.strip())
  291. ret = bytearray()
  292. whitespace = bytearray()
  293. in_quotes = False
  294. i = 0
  295. while i < len(value):
  296. c = value[i]
  297. if c == ord(b"\\"):
  298. i += 1
  299. try:
  300. v = _ESCAPE_TABLE[value[i]]
  301. except IndexError:
  302. raise ValueError(
  303. "escape character in %r at %d before end of string" % (value, i)
  304. )
  305. except KeyError:
  306. raise ValueError(
  307. "escape character followed by unknown character "
  308. "%s at %d in %r" % (value[i], i, value)
  309. )
  310. if whitespace:
  311. ret.extend(whitespace)
  312. whitespace = bytearray()
  313. ret.append(v)
  314. elif c == ord(b'"'):
  315. in_quotes = not in_quotes
  316. elif c in _COMMENT_CHARS and not in_quotes:
  317. # the rest of the line is a comment
  318. break
  319. elif c in _WHITESPACE_CHARS:
  320. whitespace.append(c)
  321. else:
  322. if whitespace:
  323. ret.extend(whitespace)
  324. whitespace = bytearray()
  325. ret.append(c)
  326. i += 1
  327. if in_quotes:
  328. raise ValueError("missing end quote")
  329. return bytes(ret)
  330. def _escape_value(value):
  331. """Escape a value."""
  332. value = value.replace(b"\\", b"\\\\")
  333. value = value.replace(b"\n", b"\\n")
  334. value = value.replace(b"\t", b"\\t")
  335. value = value.replace(b'"', b'\\"')
  336. return value
  337. def _check_variable_name(name):
  338. for i in range(len(name)):
  339. c = name[i : i + 1]
  340. if not c.isalnum() and c != b"-":
  341. return False
  342. return True
  343. def _check_section_name(name):
  344. for i in range(len(name)):
  345. c = name[i : i + 1]
  346. if not c.isalnum() and c not in (b"-", b"."):
  347. return False
  348. return True
  349. def _strip_comments(line):
  350. comment_bytes = {ord(b"#"), ord(b";")}
  351. quote = ord(b'"')
  352. string_open = False
  353. # Normalize line to bytearray for simple 2/3 compatibility
  354. for i, character in enumerate(bytearray(line)):
  355. # Comment characters outside balanced quotes denote comment start
  356. if character == quote:
  357. string_open = not string_open
  358. elif not string_open and character in comment_bytes:
  359. return line[:i]
  360. return line
  361. class ConfigFile(ConfigDict):
  362. """A Git configuration file, like .git/config or ~/.gitconfig."""
  363. def __init__(self, values=None, encoding=None):
  364. super(ConfigFile, self).__init__(values=values, encoding=encoding)
  365. self.path = None
  366. @classmethod # noqa: C901
  367. def from_file(cls, f: BinaryIO) -> "ConfigFile": # noqa: C901
  368. """Read configuration from a file-like object."""
  369. ret = cls()
  370. section = None # type: Optional[Tuple[bytes, ...]]
  371. setting = None
  372. continuation = None
  373. for lineno, line in enumerate(f.readlines()):
  374. if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
  375. line = line[3:]
  376. line = line.lstrip()
  377. if setting is None:
  378. # Parse section header ("[bla]")
  379. if len(line) > 0 and line[:1] == b"[":
  380. line = _strip_comments(line).rstrip()
  381. try:
  382. last = line.index(b"]")
  383. except ValueError:
  384. raise ValueError("expected trailing ]")
  385. pts = line[1:last].split(b" ", 1)
  386. line = line[last + 1 :]
  387. if len(pts) == 2:
  388. if pts[1][:1] != b'"' or pts[1][-1:] != b'"':
  389. raise ValueError("Invalid subsection %r" % pts[1])
  390. else:
  391. pts[1] = pts[1][1:-1]
  392. if not _check_section_name(pts[0]):
  393. raise ValueError("invalid section name %r" % pts[0])
  394. section = (pts[0], pts[1])
  395. else:
  396. if not _check_section_name(pts[0]):
  397. raise ValueError("invalid section name %r" % pts[0])
  398. pts = pts[0].split(b".", 1)
  399. if len(pts) == 2:
  400. section = (pts[0], pts[1])
  401. else:
  402. section = (pts[0],)
  403. ret._values.setdefault(section)
  404. if _strip_comments(line).strip() == b"":
  405. continue
  406. if section is None:
  407. raise ValueError("setting %r without section" % line)
  408. try:
  409. setting, value = line.split(b"=", 1)
  410. except ValueError:
  411. setting = line
  412. value = b"true"
  413. setting = setting.strip()
  414. if not _check_variable_name(setting):
  415. raise ValueError("invalid variable name %r" % setting)
  416. if value.endswith(b"\\\n"):
  417. continuation = value[:-2]
  418. else:
  419. continuation = None
  420. value = _parse_string(value)
  421. ret._values[section][setting] = value
  422. setting = None
  423. else: # continuation line
  424. if line.endswith(b"\\\n"):
  425. continuation += line[:-2]
  426. else:
  427. continuation += line
  428. value = _parse_string(continuation)
  429. ret._values[section][setting] = value
  430. continuation = None
  431. setting = None
  432. return ret
  433. @classmethod
  434. def from_path(cls, path) -> "ConfigFile":
  435. """Read configuration from a file on disk."""
  436. with GitFile(path, "rb") as f:
  437. ret = cls.from_file(f)
  438. ret.path = path
  439. return ret
  440. def write_to_path(self, path=None) -> None:
  441. """Write configuration to a file on disk."""
  442. if path is None:
  443. path = self.path
  444. with GitFile(path, "wb") as f:
  445. self.write_to_file(f)
  446. def write_to_file(self, f: BinaryIO) -> None:
  447. """Write configuration to a file-like object."""
  448. for section, values in self._values.items():
  449. try:
  450. section_name, subsection_name = section
  451. except ValueError:
  452. (section_name,) = section
  453. subsection_name = None
  454. if subsection_name is None:
  455. f.write(b"[" + section_name + b"]\n")
  456. else:
  457. f.write(b"[" + section_name + b' "' + subsection_name + b'"]\n')
  458. for key, value in values.items():
  459. if value is True:
  460. value = b"true"
  461. elif value is False:
  462. value = b"false"
  463. else:
  464. value = _format_string(value)
  465. f.write(b"\t" + key + b" = " + value + b"\n")
  466. def get_xdg_config_home_path(*path_segments):
  467. xdg_config_home = os.environ.get(
  468. "XDG_CONFIG_HOME",
  469. os.path.expanduser("~/.config/"),
  470. )
  471. return os.path.join(xdg_config_home, *path_segments)
  472. def _find_git_in_win_path():
  473. for exe in ("git.exe", "git.cmd"):
  474. for path in os.environ.get("PATH", "").split(";"):
  475. if os.path.exists(os.path.join(path, exe)):
  476. # exe path is .../Git/bin/git.exe or .../Git/cmd/git.exe
  477. git_dir, _bin_dir = os.path.split(path)
  478. yield git_dir
  479. break
  480. def _find_git_in_win_reg():
  481. import platform
  482. import winreg
  483. if platform.machine() == "AMD64":
  484. subkey = (
  485. "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\"
  486. "CurrentVersion\\Uninstall\\Git_is1"
  487. )
  488. else:
  489. subkey = (
  490. "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
  491. "Uninstall\\Git_is1"
  492. )
  493. for key in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
  494. try:
  495. with winreg.OpenKey(key, subkey) as k:
  496. val, typ = winreg.QueryValueEx(k, "InstallLocation")
  497. if typ == winreg.REG_SZ:
  498. yield val
  499. except OSError:
  500. pass
  501. # There is no set standard for system config dirs on windows. We try the
  502. # following:
  503. # - %PROGRAMDATA%/Git/config - (deprecated) Windows config dir per CGit docs
  504. # - %PROGRAMFILES%/Git/etc/gitconfig - Git for Windows (msysgit) config dir
  505. # Used if CGit installation (Git/bin/git.exe) is found in PATH in the
  506. # system registry
  507. def get_win_system_paths():
  508. if "PROGRAMDATA" in os.environ:
  509. yield os.path.join(os.environ["PROGRAMDATA"], "Git", "config")
  510. for git_dir in _find_git_in_win_path():
  511. yield os.path.join(git_dir, "etc", "gitconfig")
  512. for git_dir in _find_git_in_win_reg():
  513. yield os.path.join(git_dir, "etc", "gitconfig")
  514. class StackedConfig(Config):
  515. """Configuration which reads from multiple config files.."""
  516. def __init__(self, backends, writable=None):
  517. self.backends = backends
  518. self.writable = writable
  519. def __repr__(self):
  520. return "<%s for %r>" % (self.__class__.__name__, self.backends)
  521. @classmethod
  522. def default(cls):
  523. return cls(cls.default_backends())
  524. @classmethod
  525. def default_backends(cls):
  526. """Retrieve the default configuration.
  527. See git-config(1) for details on the files searched.
  528. """
  529. paths = []
  530. paths.append(os.path.expanduser("~/.gitconfig"))
  531. paths.append(get_xdg_config_home_path("git", "config"))
  532. if "GIT_CONFIG_NOSYSTEM" not in os.environ:
  533. paths.append("/etc/gitconfig")
  534. if sys.platform == "win32":
  535. paths.extend(get_win_system_paths())
  536. backends = []
  537. for path in paths:
  538. try:
  539. cf = ConfigFile.from_path(path)
  540. except FileNotFoundError:
  541. continue
  542. backends.append(cf)
  543. return backends
  544. def get(self, section, name):
  545. if not isinstance(section, tuple):
  546. section = (section,)
  547. for backend in self.backends:
  548. try:
  549. return backend.get(section, name)
  550. except KeyError:
  551. pass
  552. raise KeyError(name)
  553. def set(self, section, name, value):
  554. if self.writable is None:
  555. raise NotImplementedError(self.set)
  556. return self.writable.set(section, name, value)
  557. def parse_submodules(config):
  558. """Parse a gitmodules GitConfig file, returning submodules.
  559. Args:
  560. config: A `ConfigFile`
  561. Returns:
  562. list of tuples (submodule path, url, name),
  563. where name is quoted part of the section's name.
  564. """
  565. for section in config.keys():
  566. section_kind, section_name = section
  567. if section_kind == b"submodule":
  568. sm_path = config.get(section, b"path")
  569. sm_url = config.get(section, b"url")
  570. yield (sm_path, sm_url, section_name)