config.py 21 KB

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