config.py 25 KB

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