diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bafba021..e496345f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ Release 0.14.0 (unreleased) * Prevent SSH command injection (#1152) * Allow manifests with no ``projects`` key so ``dfetch add`` can bootstrap empty manifest (#1197) * Fix ``ValueError`` when generating a PackageURL (e.g. for an SBOM) from an empty or path-only remote URL +* Fix SSH shorthand URLs (``git@host:path``) being incorrectly joined with ``/`` when used as ``url-base`` with ``repo-path`` (#1247) Release 0.13.0 (released 2026-03-30) ==================================== diff --git a/dfetch/manifest/project.py b/dfetch/manifest/project.py index 5cb0c018..e3e8a281 100644 --- a/dfetch/manifest/project.py +++ b/dfetch/manifest/project.py @@ -354,6 +354,11 @@ def plaintext_warning(url: str) -> str: ) +def _is_ssh_shorthand(url: str) -> bool: + """Return True if *url* is an SSH SCP-style host-only shorthand (``git@host``, no scheme, no path separator).""" + return "@" in url and "://" not in url and ":" not in url + + @dataclass class Integrity: """Integrity verification data for an archive dependency. @@ -461,7 +466,7 @@ def set_remote(self, remote: Remote) -> None: self._remote_obj = remote self._remote = remote.name if self._url.startswith(remote.url): - self._repo_path = self._url.replace(remote.url, "").strip("/") + self._repo_path = self._url[len(remote.url) :].lstrip("/:") self._url = "" @property @@ -487,8 +492,11 @@ def remote_url(self) -> str: if self._url: return self._url if self._remote_obj: - urls = [self._remote_obj.url.strip("/"), self._repo_path] - return "/".join(urls).strip("/") + base = self._remote_obj.url.strip("/") + if not self._repo_path: + return base + separator = ":" if _is_ssh_shorthand(base) else "/" + return (base + separator + self._repo_path).strip("/") return "" @property diff --git a/tests/test_project_entry.py b/tests/test_project_entry.py index 0b560691..c8f2ca2d 100644 --- a/tests/test_project_entry.py +++ b/tests/test_project_entry.py @@ -6,6 +6,7 @@ import pytest from dfetch.manifest.project import ProjectEntry +from dfetch.manifest.remote import Remote def test_projectentry_name(): @@ -49,3 +50,45 @@ def test_projectentry_as_str(): str(ProjectEntry({"name": "SomeProject"})) == "SomeProject latest SomeProject" ) + + +def test_remote_url_ssh_shorthand_uses_colon_separator(): + entry = ProjectEntry({"name": "myrepo", "repo-path": "myorg/myrepo.git"}) + entry.set_remote(Remote({"name": "corp", "url-base": "git@git.mycompany.com"})) + assert entry.remote_url == "git@git.mycompany.com:myorg/myrepo.git" + + +def test_remote_url_https_base_uses_slash_separator(): + entry = ProjectEntry({"name": "myrepo", "repo-path": "myorg/myrepo"}) + entry.set_remote(Remote({"name": "corp", "url-base": "https://github.com"})) + assert entry.remote_url == "https://github.com/myorg/myrepo" + + +def test_set_remote_strips_colon_prefix_from_ssh_url(): + entry = ProjectEntry({"name": "myrepo", "url": "git@git.mycompany.com:my-repo.git"}) + entry.set_remote(Remote({"name": "corp", "url-base": "git@git.mycompany.com"})) + assert entry.remote_url == "git@git.mycompany.com:my-repo.git" + + +def test_set_remote_strips_slash_prefix_from_https_url(): + entry = ProjectEntry({"name": "myrepo", "url": "https://github.com/org/repo"}) + entry.set_remote(Remote({"name": "gh", "url-base": "https://github.com"})) + assert entry.remote_url == "https://github.com/org/repo" + + +def test_remote_url_with_trailing_slash_in_base(): + entry = ProjectEntry({"name": "myrepo", "repo-path": "myorg/myrepo"}) + entry.set_remote(Remote({"name": "corp", "url-base": "https://github.com/"})) + assert entry.remote_url == "https://github.com/myorg/myrepo" + + +def test_remote_url_with_empty_repo_path(): + entry = ProjectEntry({"name": "myrepo"}) + entry.set_remote(Remote({"name": "corp", "url-base": "git@git.mycompany.com"})) + assert entry.remote_url == "git@git.mycompany.com" + + +def test_remote_url_ssh_base_with_path_prefix_uses_slash_separator(): + entry = ProjectEntry({"name": "myrepo", "repo-path": "repo.git"}) + entry.set_remote(Remote({"name": "corp", "url-base": "git@host:org"})) + assert entry.remote_url == "git@host:org/repo.git"