Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
====================================
Expand Down
14 changes: 11 additions & 3 deletions dfetch/manifest/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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("/")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return ""

@property
Expand Down
43 changes: 43 additions & 0 deletions tests/test_project_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from dfetch.manifest.project import ProjectEntry
from dfetch.manifest.remote import Remote


def test_projectentry_name():
Expand Down Expand Up @@ -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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.


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"
Loading