config.py 22 KB

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