| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 563cb1f commit 833c8d8
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,7 +12,6 @@ | |||
| 12 | 12 | import shlex | |
| 13 | 13 | import stat | |
| 14 | 14 | import sys | |
| 15 | - import urllib.parse | ||
| 16 | 15 | import uuid | |
| 17 | 16 | from io import BytesIO | |
| 18 | 17 | from pathlib import Path | |
@@ -58,7 +57,9 @@ | |||
| 58 | 57 | from .util import ( | |
| 59 | 58 | SubmoduleConfigParser, | |
| 60 | 59 | find_first_remote_branch, | |
| 60 | + is_relative_url, | ||
| 61 | 61 | mkhead, | |
| 62 | + resolve_relative_url, | ||
| 62 | 63 | sm_name, | |
| 63 | 64 | sm_section, | |
| 64 | 65 | ) | |
@@ -428,10 +429,10 @@ def _clone_repo( | |||
| 428 | 429 | if not osp.isdir(module_abspath_dir): | |
| 429 | 430 | os.makedirs(module_abspath_dir) | |
| 430 | 431 | ||
| 431 | - if url.startswith("../"): | ||
| 432 | + if is_relative_url(url): | ||
| 432 | 433 | remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name | |
| 433 | 434 | repo_remote_url = repo.remote(remote_name).url | |
| 434 | - url = os.path.join(repo_remote_url, url) | ||
| 435 | + url = resolve_relative_url(repo_remote_url, url) | ||
| 435 | 436 | ||
| 436 | 437 | clone = git.Repo.clone_from( | |
| 437 | 438 | url, | |
@@ -982,13 +983,9 @@ def fetch_remotes(module_repo: "Repo") -> None: | |||
| 982 | 983 | if not dry_run: | |
| 983 | 984 | if no_fetch: | |
| 984 | 985 | raise ValueError("Missing module at %r but fetching is disabled" % self.path) from None | |
| 985 | - if self.url.startswith("."): | ||
| 986 | - url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url) | ||
| 987 | - else: | ||
| 988 | - url = self.url | ||
| 989 | 986 | mrepo = self._clone_repo( | |
| 990 | 987 | self.repo, | |
| 991 | - url, | ||
| 988 | + self._resolved_url(), | ||
| 992 | 989 | self.path, | |
| 993 | 990 | self.name, | |
| 994 | 991 | n=True, | |
@@ -1738,6 +1735,13 @@ def url(self) -> str: | |||
| 1738 | 1735 | """:return: The url to the repository our submodule's repository refers to""" | |
| 1739 | 1736 | return self._url | |
| 1740 | 1737 | ||
| 1738 | + def _resolved_url(self) -> str: | ||
| 1739 | + """:return: :attr:`url`, with a ``./`` or ``../`` prefix resolved against the | ||
| 1740 | + url of the superproject's ``origin`` remote the way git does""" | ||
| 1741 | + if not is_relative_url(self.url): | ||
| 1742 | + return self.url | ||
| 1743 | + return resolve_relative_url(self.repo.remotes.origin.url, self.url) | ||
| 1744 | + | ||
| 1741 | 1745 | @property | |
| 1742 | 1746 | def parent_commit(self) -> "Commit": | |
| 1743 | 1747 | """ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -264,10 +264,14 @@ def update( # type: ignore[override] | |||
| 264 | 264 | nn = "__new_origin__" | |
| 265 | 265 | smm = sm.module() | |
| 266 | 266 | rmts = smm.remotes | |
| 267 | + # Relative urls are resolved against the superproject's remote, | ||
| 268 | + # which is what the module's remotes hold. | ||
| 269 | + sm_url = sm._resolved_url() | ||
| 270 | + psm_url = psm._resolved_url() | ||
| 267 | 271 | ||
| 268 | 272 | # Don't do anything if we already have the url we search in | |
| 269 | 273 | # place. | |
| 270 | - if len([r for r in rmts if r.url == sm.url]) == 0: | ||
| 274 | + if len([r for r in rmts if r.url == sm_url]) == 0: | ||
| 271 | 275 | progress.update( | |
| 272 | 276 | BEGIN | URLCHANGE, | |
| 273 | 277 | i, | |
@@ -278,7 +282,7 @@ def update( # type: ignore[override] | |||
| 278 | 282 | if not dry_run: | |
| 279 | 283 | previous_remote = None | |
| 280 | 284 | for remote in rmts: | |
| 281 | - if remote.url == psm.url: | ||
| 285 | + if remote.url == psm_url: | ||
| 282 | 286 | previous_remote = remote | |
| 283 | 287 | break | |
| 284 | 288 | # END if urls match | |
@@ -305,10 +309,10 @@ def update( # type: ignore[override] | |||
| 305 | 309 | # A new remote would have no cached refs. Preserve | |
| 306 | 310 | # the existing refs and tracking configuration for | |
| 307 | 311 | # offline updates instead of replacing the remote. | |
| 308 | - previous_remote.set_url(git.Git.polish_url(sm.url, expand_vars=False)) | ||
| 312 | + previous_remote.set_url(git.Git.polish_url(sm_url, expand_vars=False)) | ||
| 309 | 313 | else: | |
| 310 | 314 | assert nn not in [r.name for r in rmts] | |
| 311 | - smr = smm.create_remote(nn, sm.url) | ||
| 315 | + smr = smm.create_remote(nn, sm_url) | ||
| 312 | 316 | smr.fetch(progress=progress) | |
| 313 | 317 | ||
| 314 | 318 | # If we have a tracking branch, it should be available | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,10 +6,13 @@ | |||
| 6 | 6 | "sm_name", | |
| 7 | 7 | "mkhead", | |
| 8 | 8 | "find_first_remote_branch", | |
| 9 | + "is_relative_url", | ||
| 10 | + "resolve_relative_url", | ||
| 9 | 11 | "SubmoduleConfigParser", | |
| 10 | 12 | ] | |
| 11 | 13 | ||
| 12 | 14 | from io import BytesIO | |
| 15 | + import sys | ||
| 13 | 16 | import weakref | |
| 14 | 17 | ||
| 15 | 18 | import git | |
@@ -63,6 +66,97 @@ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> " | |||
| 63 | 66 | raise InvalidGitRepositoryError("Didn't find remote branch '%r' in any of the given remotes" % branch_name) | |
| 64 | 67 | ||
| 65 | 68 | ||
| 69 | + def _is_dir_sep(char: str) -> bool: | ||
| 70 | + return char == "/" or (sys.platform == "win32" and char == "\\") | ||
| 71 | + | ||
| 72 | + | ||
| 73 | + def _starts_with_dot_slash(url: str) -> bool: | ||
| 74 | + return len(url) >= 2 and url[0] == "." and _is_dir_sep(url[1]) | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + def _starts_with_dot_dot_slash(url: str) -> bool: | ||
| 78 | + return len(url) >= 3 and url[0:2] == ".." and _is_dir_sep(url[2]) | ||
| 79 | + | ||
| 80 | + | ||
| 81 | + def _has_dos_drive_prefix(url: str) -> bool: | ||
| 82 | + return sys.platform == "win32" and len(url) >= 2 and url[0].isalpha() and url[1] == ":" | ||
| 83 | + | ||
| 84 | + | ||
| 85 | + def _is_absolute_path(url: str) -> bool: | ||
| 86 | + return (len(url) >= 1 and _is_dir_sep(url[0])) or _has_dos_drive_prefix(url) | ||
| 87 | + | ||
| 88 | + | ||
| 89 | + def _url_is_local_not_ssh(url: str) -> bool: | ||
| 90 | + colon = url.find(":") | ||
| 91 | + slash = url.find("/") | ||
| 92 | + return colon == -1 or (slash != -1 and slash < colon) or _has_dos_drive_prefix(url) | ||
| 93 | + | ||
| 94 | + | ||
| 95 | + def _find_last_dir_sep(url: str) -> int: | ||
| 96 | + for index in range(len(url) - 1, -1, -1): | ||
| 97 | + if _is_dir_sep(url[index]): | ||
| 98 | + return index | ||
| 99 | + return -1 | ||
| 100 | + | ||
| 101 | + | ||
| 102 | + def is_relative_url(url: str) -> bool: | ||
| 103 | + """:return: ``True`` if `url` starts with ``./`` or ``../``, which git resolves | ||
| 104 | + against the superproject's remote rather than treating as a path""" | ||
| 105 | + return _starts_with_dot_slash(url) or _starts_with_dot_dot_slash(url) | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + def resolve_relative_url(remote_url: str, url: str) -> str: | ||
| 109 | + """Resolve a submodule URL that starts with ``./`` or ``../`` against the | ||
| 110 | + superproject's remote URL, the way git's ``relative_url`` does. | ||
| 111 | + | ||
| 112 | + Git treats a relative submodule URL like a relative directory: each leading | ||
| 113 | + ``../`` removes the last component of `remote_url`, whether that is a path | ||
| 114 | + component, the host of a URL, or the host of an scp-like ``host:path`` address. | ||
| 115 | + The remainder is appended with ``/``, or with ``:`` once an scp-like host was | ||
| 116 | + reached. A URL that is not relative is returned unchanged. | ||
| 117 | + | ||
| 118 | + :raise ValueError: | ||
| 119 | + If more components are stripped than `remote_url` has, which git rejects with | ||
| 120 | + "cannot strip one component off url". | ||
| 121 | + """ | ||
| 122 | + if not is_relative_url(url): | ||
| 123 | + return url | ||
| 124 | + | ||
| 125 | + remote = remote_url | ||
| 126 | + if remote and _is_dir_sep(remote[-1]): | ||
| 127 | + remote = remote[:-1] | ||
| 128 | + | ||
| 129 | + is_relative = _url_is_local_not_ssh(remote) and not _is_absolute_path(remote) | ||
| 130 | + if is_relative and not _starts_with_dot_slash(remote) and not _starts_with_dot_dot_slash(remote): | ||
| 131 | + remote = "./" + remote | ||
| 132 | + | ||
| 133 | + colonsep = False | ||
| 134 | + while url: | ||
| 135 | + if _starts_with_dot_dot_slash(url): | ||
| 136 | + url = url[3:] | ||
| 137 | + separator = _find_last_dir_sep(remote) | ||
| 138 | + if separator != -1: | ||
| 139 | + remote = remote[:separator] | ||
| 140 | + elif ":" in remote: | ||
| 141 | + remote = remote[: remote.rfind(":")] | ||
| 142 | + colonsep = True | ||
| 143 | + elif is_relative or remote == ".": | ||
| 144 | + raise ValueError("cannot strip one component off url %r" % remote) | ||
| 145 | + else: | ||
| 146 | + remote = "." | ||
| 147 | + elif _starts_with_dot_slash(url): | ||
| 148 | + url = url[2:] | ||
| 149 | + else: | ||
| 150 | + break | ||
| 151 | + | ||
| 152 | + resolved = remote + (":" if colonsep else "/") + url | ||
| 153 | + if url.endswith("/"): | ||
| 154 | + resolved = resolved[:-1] | ||
| 155 | + if _starts_with_dot_slash(resolved): | ||
| 156 | + resolved = resolved[2:] | ||
| 157 | + return resolved | ||
| 158 | + | ||
| 159 | + | ||
| 66 | 160 | # } END utilities | |
| 67 | 161 | ||
| 68 | 162 | # { Classes | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,6 +26,7 @@ | |||
| 26 | 26 | ) | |
| 27 | 27 | from git.objects.submodule.base import Submodule | |
| 28 | 28 | from git.objects.submodule.root import RootModule, RootUpdateProgress | |
| 29 | + from git.objects.submodule.util import resolve_relative_url | ||
| 29 | 30 | from git.repo.fun import find_submodule_git_dir, touch | |
| 30 | 31 | from git.util import HIDE_WINDOWS_KNOWN_ERRORS, cwd, join_path_native, to_native_path_linux | |
| 31 | 32 | ||
@@ -2416,3 +2417,65 @@ def test_submodule_update_relative_url(self, rwdir): | |||
| 2416 | 2417 | ||
| 2417 | 2418 | has_module = any(sm.name == "module" for sm in cloned_repo.submodules) | |
| 2418 | 2419 | assert has_module, "Relative submodule was not updated properly" | |
| 2420 | + | ||
| 2421 | + def test_resolve_relative_url_like_git(self): | ||
| 2422 | + # Expected values are what `git submodule init` records for the same pair. | ||
| 2423 | + cases = [ | ||
| 2424 | + ("ssh://host/org/super.git", "./sub", "ssh://host/org/super.git/sub"), | ||
| 2425 | + ("ssh://host/org/super.git", "../sub", "ssh://host/org/sub"), | ||
| 2426 | + ("ssh://host:22/org/super.git", "../sub", "ssh://host:22/org/sub"), | ||
| 2427 | + ("git@host:org/super.git", "./sub", "git@host:org/super.git/sub"), | ||
| 2428 | + ("git@host:org/super.git", "../sub", "git@host:org/sub"), | ||
| 2429 | + ("git@host:org/super.git", "../../sub", "git@host:sub"), | ||
| 2430 | + ("https://host/org/super.git", "./sub/", "https://host/org/super.git/sub"), | ||
| 2431 | + ("https://host/org/super.git/", "././sub", "https://host/org/super.git/sub"), | ||
| 2432 | + ("https://host/org/super.git", ".././sub", "https://host/org/sub"), | ||
| 2433 | + ("https://host/org/super.git", "../../../sub", "https://sub"), | ||
| 2434 | + ("file:///srv/git/super.git", "../sub", "file:///srv/git/sub"), | ||
| 2435 | + ("/srv/git/super", "../sub", "/srv/git/sub"), | ||
| 2436 | + ("/srv/git/super", "../../../../sub", "sub"), | ||
| 2437 | + ("//share/super", "../../sub", "//sub"), | ||
| 2438 | + ("foo/bar", "../sub", "foo/sub"), | ||
| 2439 | + ("./foo", "../sub", "sub"), | ||
| 2440 | + ("../foo/bar", "../sub", "../foo/sub"), | ||
| 2441 | + ("ssh://host/org/super.git", ".sub", ".sub"), | ||
| 2442 | + ("ssh://host/org/super.git", "https://other/sub", "https://other/sub"), | ||
| 2443 | + ] | ||
| 2444 | + for remote_url, url, expected in cases: | ||
| 2445 | + assert resolve_relative_url(remote_url, url) == expected, (remote_url, url) | ||
| 2446 | + | ||
| 2447 | + for remote_url, url in [("foo", "../../sub"), ("git@host:super.git", "../../../sub"), ("../foo", "../../sub")]: | ||
| 2448 | + with self.assertRaisesRegex(ValueError, "cannot strip one component"): | ||
| 2449 | + resolve_relative_url(remote_url, url) | ||
| 2450 | + | ||
| 2451 | + @with_rw_directory | ||
| 2452 | + def test_update_resolves_relative_url_against_remote(self, rwdir): | ||
| 2453 | + parent = git.Repo.init(osp.join(rwdir, "parent")) | ||
| 2454 | + parent.create_remote("origin", "ssh://git@host/org/super.git") | ||
| 2455 | + os.mkdir(osp.join(parent.working_tree_dir, "planted")) | ||
| 2456 | + | ||
| 2457 | + for url, expected in [ | ||
| 2458 | + ("./planted", "ssh://git@host/org/super.git/planted"), | ||
| 2459 | + ("../planted", "ssh://git@host/org/planted"), | ||
| 2460 | + ]: | ||
| 2461 | + submodule = Submodule(parent, Submodule.NULL_BIN_SHA, name="module", path="module", url=url) | ||
| 2462 | + with mock.patch.object(Submodule, "_clone_repo", side_effect=AssertionError("stop")) as clone_repo: | ||
| 2463 | + with pytest.raises(AssertionError, match="stop"): | ||
| 2464 | + submodule.update(init=True) | ||
| 2465 | + assert clone_repo.call_args.args[1] == expected | ||
| 2466 | + | ||
| 2467 | + @with_rw_directory | ||
| 2468 | + def test_clone_repo_resolves_relative_url_against_tracking_remote(self, rwdir): | ||
| 2469 | + parent = git.Repo.init(osp.join(rwdir, "parent")) | ||
| 2470 | + parent.index.commit("initial") | ||
| 2471 | + parent.create_remote("origin", "git@host:org/super.git") | ||
| 2472 | + parent.head.reference.set_tracking_branch(git.RemoteReference(parent, "refs/remotes/origin/main")) | ||
| 2473 | + | ||
| 2474 | + for url, expected in [ | ||
| 2475 | + ("./planted", "git@host:org/super.git/planted"), | ||
| 2476 | + ("../planted", "git@host:org/planted"), | ||
| 2477 | + ]: | ||
| 2478 | + with mock.patch.object(git.Repo, "clone_from", side_effect=AssertionError("stop")) as clone_from: | ||
| 2479 | + with pytest.raises(AssertionError, match="stop"): | ||
| 2480 | + Submodule._clone_repo(parent, url, "module", "module") | ||
| 2481 | + assert clone_from.call_args.args[0] == expected | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments