config.py 24 KB

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