commit bf03e2e836549cebc2bea9f9909649885ceffaa4 from: Jelmer Vernooij date: Fri Jun 24 14:30:18 2022 UTC Support applying of URL rewriting using insteadOf/pushInsteadOf. Fixes #706 commit - 55f151dd04897f8d20a8c7e3a5a4ff89d6f1cfc4 commit + bf03e2e836549cebc2bea9f9909649885ceffaa4 blob - ffb09bf2e31af7558226378fc160a1243da68c67 blob + db78a5d8d4be44d580f33820ffdfa580ce23efd5 --- NEWS +++ NEWS @@ -2,6 +2,9 @@ * Fix reading of chunks in server. (Jelmer Vernooij, #977) + * Support applying of URL rewriting using ``insteadOf`` / ``pushInsteadOf``. + (Jelmer Vernooij, #706) + 0.20.43 2022-06-07 * Lazily import url2pathname. blob - 3e42df1110827c2d294370cb448591d31dc71c55 blob + 19f540e69893a800afb90122765f0e02d003faf1 --- dulwich/client.py +++ dulwich/client.py @@ -46,7 +46,7 @@ import select import socket import subprocess import sys -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, IO +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, IO, Iterable from urllib.parse import ( quote as urlquote, @@ -59,7 +59,7 @@ from urllib.parse import ( import dulwich -from dulwich.config import get_xdg_config_home_path +from dulwich.config import get_xdg_config_home_path, Config from dulwich.errors import ( GitProtocolError, NotGitRepository, @@ -2268,12 +2268,49 @@ def _win32_url_to_path(parsed) -> str: return url2pathname(netloc + path) # type: ignore -def get_transport_and_path_from_url(url, config=None, **kwargs): +def iter_instead_of(config: Config, push: bool = False) -> Iterable[Tuple[str, str]]: + """Iterate over insteadOf / pushInsteadOf values. + """ + for section in config.sections(): + if section[0] != b'url': + continue + replacement = section[1] + try: + needles = list(config.get_multivar(section, "insteadOf")) + except KeyError: + needles = [] + if push: + try: + needles += list(config.get_multivar(section, "pushInsteadOf")) + except KeyError: + pass + for needle in needles: + yield needle.decode('utf-8'), replacement.decode('utf-8') + + +def apply_instead_of(config: Config, orig_url: str, push: bool = False) -> str: + """Apply insteadOf / pushInsteadOf to a URL. + """ + longest_needle = "" + updated_url = orig_url + for needle, replacement in iter_instead_of(config, push): + if not orig_url.startswith(needle): + continue + if len(longest_needle) < len(needle): + longest_needle = needle + updated_url = replacement + orig_url[len(needle):] + return updated_url + + +def get_transport_and_path_from_url( + url: str, config: Optional[Config] = None, + operation: Optional[str] = None, **kwargs) -> Tuple[GitClient, str]: """Obtain a git client from a URL. Args: url: URL to open (a unicode string) config: Optional config object + operation: Kind of operation that'll be performed; "pull" or "push" thin_packs: Whether or not thin packs should be retrieved report_activity: Optional callback for reporting transport activity. @@ -2282,6 +2319,8 @@ def get_transport_and_path_from_url(url, config=None, Tuple with client instance and relative path. """ + if config is not None: + url = apply_instead_of(config, url, push=(operation == "push")) parsed = urlparse(url) if parsed.scheme == "git": return (TCPGitClient.from_parsedurl(parsed, **kwargs), parsed.path) @@ -2303,7 +2342,7 @@ def get_transport_and_path_from_url(url, config=None, raise ValueError("unknown scheme '%s'" % parsed.scheme) -def parse_rsync_url(location): +def parse_rsync_url(location: str) -> Tuple[Optional[str], str, str]: """Parse a rsync-style URL.""" if ":" in location and "@" not in location: # SSH with no user@, zero or one leading slash. @@ -2324,6 +2363,7 @@ def parse_rsync_url(location): def get_transport_and_path( location: str, + operation: Optional[str] = None, **kwargs: Any ) -> Tuple[GitClient, str]: """Obtain a git client from a URL. @@ -2331,6 +2371,7 @@ def get_transport_and_path( Args: location: URL or path (a string) config: Optional config object + operation: Kind of operation that'll be performed; "pull" or "push" thin_packs: Whether or not thin packs should be retrieved report_activity: Optional callback for reporting transport activity. @@ -2341,7 +2382,7 @@ def get_transport_and_path( """ # First, try to parse it as a URL try: - return get_transport_and_path_from_url(location, **kwargs) + return get_transport_and_path_from_url(location, operation=operation, **kwargs) except ValueError: pass blob - 4ceecab182fd7d58ab8dab0dd4d0d18371d75afc blob + 30b67fb3173e2adf104c10ee6f23c8b697b8b6d4 --- dulwich/config.py +++ dulwich/config.py @@ -337,7 +337,7 @@ class ConfigDict(Config, MutableMapping[Key, MutableMa if len(section) > 1: try: - return self._values[section][name] + return self._values[section].get_all(name) except KeyError: pass @@ -708,12 +708,29 @@ class StackedConfig(Config): pass raise KeyError(name) + def get_multivar(self, section, name): + if not isinstance(section, tuple): + section = (section,) + for backend in self.backends: + try: + yield from backend.get_multivar(section, name) + except KeyError: + pass + def set(self, section, name, value): if self.writable is None: raise NotImplementedError(self.set) return self.writable.set(section, name, value) + def sections(self): + seen = set() + for backend in self.backends: + for section in backend.sections(): + if section not in seen: + seen.add(section) + yield section + def parse_submodules(config: ConfigFile) -> Iterator[Tuple[bytes, bytes, bytes]]: """Parse a gitmodules GitConfig file, returning submodules. blob - 767fe94a49c684c5c481bf515a8b19c4892dfe1c blob + d8c00b557f47b03af641d3e2624c4142616b8c12 --- dulwich/tests/test_client.py +++ dulwich/tests/test_client.py @@ -52,6 +52,7 @@ from dulwich.client import ( PLinkSSHVendor, HangupException, GitProtocolError, + apply_instead_of, check_wants, default_urllib3_manager, get_credentials_from_store, @@ -1615,3 +1616,31 @@ And this line is just random noise, too. ] ), ) + + +class ApplyInsteadOfTests(TestCase): + def test_none(self): + config = ConfigDict() + self.assertEqual( + 'https://example.com/', apply_instead_of(config, 'https://example.com/')) + + def test_apply(self): + config = ConfigDict() + config.set( + ('url', 'https://samba.org/'), 'insteadOf', 'https://example.com/') + self.assertEqual( + 'https://samba.org/', + apply_instead_of(config, 'https://example.com/')) + + def test_apply_multiple(self): + config = ConfigDict() + config.set( + ('url', 'https://samba.org/'), 'insteadOf', 'https://blah.com/') + config.set( + ('url', 'https://samba.org/'), 'insteadOf', 'https://example.com/') + self.assertEqual( + [b'https://blah.com/', b'https://example.com/'], + list(config.get_multivar(('url', 'https://samba.org/'), 'insteadOf'))) + self.assertEqual( + 'https://samba.org/', + apply_instead_of(config, 'https://example.com/'))