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 (
  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:
  328. raise ValueError(
  329. "escape character in %r at %d before end of string" % (value, i)
  330. )
  331. except KeyError:
  332. raise ValueError(
  333. "escape character followed by unknown character "
  334. "%s at %d in %r" % (value[i], i, value)
  335. )
  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"\n", b"\\n")
  360. value = value.replace(b"\t", b"\\t")
  361. value = value.replace(b'"', b'\\"')
  362. return value
  363. def _check_variable_name(name: bytes) -> bool:
  364. for i in range(len(name)):
  365. c = name[i : i + 1]
  366. if not c.isalnum() and c != b"-":
  367. return False
  368. return True
  369. def _check_section_name(name: bytes) -> bool:
  370. for i in range(len(name)):
  371. c = name[i : i + 1]
  372. if not c.isalnum() and c not in (b"-", b"."):
  373. return False
  374. return True
  375. def _strip_comments(line: bytes) -> bytes:
  376. comment_bytes = {ord(b"#"), ord(b";")}
  377. quote = ord(b'"')
  378. string_open = False
  379. # Normalize line to bytearray for simple 2/3 compatibility
  380. for i, character in enumerate(bytearray(line)):
  381. # Comment characters outside balanced quotes denote comment start
  382. if character == quote:
  383. string_open = not string_open
  384. elif not string_open and character in comment_bytes:
  385. return line[:i]
  386. return line
  387. def _parse_section_header_line(line: bytes) -> Tuple[Section, bytes]:
  388. # Parse section header ("[bla]")
  389. line = _strip_comments(line).rstrip()
  390. in_quotes = False
  391. escaped = False
  392. for i, c in enumerate(line):
  393. if escaped:
  394. escaped = False
  395. continue
  396. if c == ord(b'"'):
  397. in_quotes = not in_quotes
  398. if c == ord(b'\\'):
  399. escaped = True
  400. if c == ord(b']') and not in_quotes:
  401. last = i
  402. break
  403. else:
  404. raise ValueError("expected trailing ]")
  405. pts = line[1:last].split(b" ", 1)
  406. line = line[last + 1:]
  407. section: Section
  408. if len(pts) == 2:
  409. if pts[1][:1] != b'"' or pts[1][-1:] != b'"':
  410. raise ValueError("Invalid subsection %r" % pts[1])
  411. else:
  412. pts[1] = pts[1][1:-1]
  413. if not _check_section_name(pts[0]):
  414. raise ValueError("invalid section name %r" % pts[0])
  415. section = (pts[0], pts[1])
  416. else:
  417. if not _check_section_name(pts[0]):
  418. raise ValueError("invalid section name %r" % pts[0])
  419. pts = pts[0].split(b".", 1)
  420. if len(pts) == 2:
  421. section = (pts[0], pts[1])
  422. else:
  423. section = (pts[0],)
  424. return section, line
  425. class ConfigFile(ConfigDict):
  426. """A Git configuration file, like .git/config or ~/.gitconfig."""
  427. def __init__(
  428. self,
  429. values: Union[
  430. MutableMapping[Section, MutableMapping[Name, Value]], None
  431. ] = None,
  432. encoding: Union[str, None] = None
  433. ) -> None:
  434. super(ConfigFile, self).__init__(values=values, encoding=encoding)
  435. self.path: Optional[str] = None
  436. @classmethod # noqa: C901
  437. def from_file(cls, f: BinaryIO) -> "ConfigFile": # noqa: C901
  438. """Read configuration from a file-like object."""
  439. ret = cls()
  440. section: Optional[Section] = None
  441. setting = None
  442. continuation = None
  443. for lineno, line in enumerate(f.readlines()):
  444. if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
  445. line = line[3:]
  446. line = line.lstrip()
  447. if setting is None:
  448. if len(line) > 0 and line[:1] == b"[":
  449. section, line = _parse_section_header_line(line)
  450. ret._values.setdefault(section)
  451. if _strip_comments(line).strip() == b"":
  452. continue
  453. if section is None:
  454. raise ValueError("setting %r without section" % line)
  455. try:
  456. setting, value = line.split(b"=", 1)
  457. except ValueError:
  458. setting = line
  459. value = b"true"
  460. setting = setting.strip()
  461. if not _check_variable_name(setting):
  462. raise ValueError("invalid variable name %r" % setting)
  463. if value.endswith(b"\\\n"):
  464. continuation = value[:-2]
  465. else:
  466. continuation = None
  467. value = _parse_string(value)
  468. ret._values[section][setting] = value
  469. setting = None
  470. else: # continuation line
  471. if line.endswith(b"\\\n"):
  472. continuation += line[:-2]
  473. else:
  474. continuation += line
  475. value = _parse_string(continuation)
  476. ret._values[section][setting] = value
  477. continuation = None
  478. setting = None
  479. return ret
  480. @classmethod
  481. def from_path(cls, path: str) -> "ConfigFile":
  482. """Read configuration from a file on disk."""
  483. with GitFile(path, "rb") as f:
  484. ret = cls.from_file(f)
  485. ret.path = path
  486. return ret
  487. def write_to_path(self, path: Optional[str] = None) -> None:
  488. """Write configuration to a file on disk."""
  489. if path is None:
  490. path = self.path
  491. with GitFile(path, "wb") as f:
  492. self.write_to_file(f)
  493. def write_to_file(self, f: BinaryIO) -> None:
  494. """Write configuration to a file-like object."""
  495. for section, values in self._values.items():
  496. try:
  497. section_name, subsection_name = section
  498. except ValueError:
  499. (section_name,) = section
  500. subsection_name = None
  501. if subsection_name is None:
  502. f.write(b"[" + section_name + b"]\n")
  503. else:
  504. f.write(b"[" + section_name + b' "' + subsection_name + b'"]\n')
  505. for key, value in values.items():
  506. value = _format_string(value)
  507. f.write(b"\t" + key + b" = " + value + b"\n")
  508. def get_xdg_config_home_path(*path_segments):
  509. xdg_config_home = os.environ.get(
  510. "XDG_CONFIG_HOME",
  511. os.path.expanduser("~/.config/"),
  512. )
  513. return os.path.join(xdg_config_home, *path_segments)
  514. def _find_git_in_win_path():
  515. for exe in ("git.exe", "git.cmd"):
  516. for path in os.environ.get("PATH", "").split(";"):
  517. if os.path.exists(os.path.join(path, exe)):
  518. # exe path is .../Git/bin/git.exe or .../Git/cmd/git.exe
  519. git_dir, _bin_dir = os.path.split(path)
  520. yield git_dir
  521. break
  522. def _find_git_in_win_reg():
  523. import platform
  524. import winreg
  525. if platform.machine() == "AMD64":
  526. subkey = (
  527. "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\"
  528. "CurrentVersion\\Uninstall\\Git_is1"
  529. )
  530. else:
  531. subkey = (
  532. "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
  533. "Uninstall\\Git_is1"
  534. )
  535. for key in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
  536. try:
  537. with winreg.OpenKey(key, subkey) as k:
  538. val, typ = winreg.QueryValueEx(k, "InstallLocation")
  539. if typ == winreg.REG_SZ:
  540. yield val
  541. except OSError:
  542. pass
  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 "<%s for %r>" % (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 parse_submodules(config: ConfigFile) -> Iterator[Tuple[bytes, bytes, bytes]]:
  622. """Parse a gitmodules GitConfig file, returning submodules.
  623. Args:
  624. config: A `ConfigFile`
  625. Returns:
  626. list of tuples (submodule path, url, name),
  627. where name is quoted part of the section's name.
  628. """
  629. for section in config.keys():
  630. section_kind, section_name = section
  631. if section_kind == b"submodule":
  632. sm_path = config.get(section, b"path")
  633. sm_url = config.get(section, b"url")
  634. yield (sm_path, sm_url, section_name)
  635. def iter_instead_of(config: Config, push: bool = False) -> Iterable[Tuple[str, str]]:
  636. """Iterate over insteadOf / pushInsteadOf values.
  637. """
  638. for section in config.sections():
  639. if section[0] != b'url':
  640. continue
  641. replacement = section[1]
  642. try:
  643. needles = list(config.get_multivar(section, "insteadOf"))
  644. except KeyError:
  645. needles = []
  646. if push:
  647. try:
  648. needles += list(config.get_multivar(section, "pushInsteadOf"))
  649. except KeyError:
  650. pass
  651. for needle in needles:
  652. assert isinstance(needle, bytes)
  653. yield needle.decode('utf-8'), replacement.decode('utf-8')
  654. def apply_instead_of(config: Config, orig_url: str, push: bool = False) -> str:
  655. """Apply insteadOf / pushInsteadOf to a URL.
  656. """
  657. longest_needle = ""
  658. updated_url = orig_url
  659. for needle, replacement in iter_instead_of(config, push):
  660. if not orig_url.startswith(needle):
  661. continue
  662. if len(longest_needle) < len(needle):
  663. longest_needle = needle
  664. updated_url = replacement + orig_url[len(needle):]
  665. return updated_url