2
0

config.py 24 KB

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