config.py 25 KB

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