diff --git a/TODO b/TODO index 82a0626..5cb99ce 100644 --- a/TODO +++ b/TODO @@ -284,7 +284,7 @@ Content-Type: application/issue ID: 21 Type: feature Title: relax host restriction in vcs.git parse_base_url and parse_repo_name -Status: in-progress +Status: done Priority: high Created: 2026-06-06 Relationships: diff --git a/src/byteb4rb1e/utils/vcs/git.py b/src/byteb4rb1e/utils/vcs/git.py index cd4cd87..9133794 100644 --- a/src/byteb4rb1e/utils/vcs/git.py +++ b/src/byteb4rb1e/utils/vcs/git.py @@ -26,54 +26,40 @@ class GitError(Exception): def parse_base_url(base_url: str) -> str: - """Extract workspace from an SCP-style Bitbucket base URL. + """Extract the workspace from an SCP-style base URL. - The host part must be exactly ``bitbucket.org`` — bootstrapping - requires the Bitbucket API, so other hosts are rejected. + Accepts any host (Bitbucket, Forgejo, GitHub, ...) as long as + the URL is SCP-style:: - >>> _parse_base_url("git@bitbucket.org:byteb4rb1e") - 'byteb4rb1e' + git@bitbucket.org:byteb4rb1e/foo.git → byteb4rb1e + git@git.code.tiararodney.com:h5p-mirror/foo.git → h5p-mirror """ - # SCP-style: git@bitbucket.org:workspace + # SCP-style: git@host:workspace/repo if ":" not in base_url or "//" in base_url: raise ValueError( - f"Expected SCP-style URL (git@bitbucket.org:workspace), " + f"Expected SCP-style URL (git@host:workspace), " f"got: {base_url}" ) - host_part, workspace = base_url.split(":", 1) - # host_part is e.g. "git@bitbucket.org" - host = host_part.split("@", 1)[-1] - if host != "bitbucket.org": - raise ValueError( - f"Mirror base URL must target bitbucket.org, " - f"got host: {host}" - ) - return Path(workspace).parent + _, workspace = base_url.split(":", 1) + return str(Path(workspace).parent) def parse_repo_name(base_url: str) -> str: - """Extract workspace from an SCP-style Bitbucket base URL. + """Extract the repository name from an SCP-style base URL. - The host part must be exactly ``bitbucket.org`` — bootstrapping - requires the Bitbucket API, so other hosts are rejected. + Accepts any host (Bitbucket, Forgejo, GitHub, ...) as long as + the URL is SCP-style:: - >>> _parse_base_url("git@bitbucket.org:byteb4rb1e") - 'byteb4rb1e' + git@bitbucket.org:byteb4rb1e/foo.git → foo + git@git.code.tiararodney.com:h5p-mirror/foo.git → foo """ - # SCP-style: git@bitbucket.org:workspace + # SCP-style: git@host:workspace/repo if ":" not in base_url or "//" in base_url: raise ValueError( - f"Expected SCP-style URL (git@bitbucket.org:workspace), " + f"Expected SCP-style URL (git@host:workspace), " f"got: {base_url}" ) - host_part, workspace = base_url.split(":", 1) - # host_part is e.g. "git@bitbucket.org" - host = host_part.split("@", 1)[-1] - if host != "bitbucket.org": - raise ValueError( - f"Mirror base URL must target bitbucket.org, " - f"got host: {host}" - ) + _, workspace = base_url.split(":", 1) return Path(workspace).name.split('.')[0] diff --git a/tests/unit/byteb4rb1e/utils/vcs/test_git.py b/tests/unit/byteb4rb1e/utils/vcs/test_git.py new file mode 100644 index 0000000..e60a78e --- /dev/null +++ b/tests/unit/byteb4rb1e/utils/vcs/test_git.py @@ -0,0 +1,60 @@ +"""Tests for the git subprocess wrapper's URL parsing helpers.""" + +import pytest + +from byteb4rb1e.utils.vcs.git import parse_base_url, parse_repo_name + + +class TestParseBaseUrl: + + def test_bitbucket(self) -> None: + result = parse_base_url("git@bitbucket.org:byteb4rb1e/foo.git") + assert result == "byteb4rb1e" + + def test_forgejo_host(self) -> None: + result = parse_base_url( + "git@git.code.tiararodney.com:h5p-mirror/foo.git" + ) + assert result == "h5p-mirror" + + def test_github_host(self) -> None: + result = parse_base_url("git@github.com:h5p/h5p-multi-choice.git") + assert result == "h5p" + + def test_returns_str(self) -> None: + result = parse_base_url("git@bitbucket.org:byteb4rb1e/foo.git") + assert isinstance(result, str) + + def test_rejects_https_url(self) -> None: + with pytest.raises(ValueError): + parse_base_url("https://bitbucket.org/byteb4rb1e/foo.git") + + def test_rejects_url_without_colon(self) -> None: + with pytest.raises(ValueError): + parse_base_url("bitbucket.org/byteb4rb1e/foo.git") + + +class TestParseRepoName: + + def test_bitbucket(self) -> None: + assert parse_repo_name( + "git@bitbucket.org:byteb4rb1e/foo.git" + ) == "foo" + + def test_forgejo_host(self) -> None: + assert parse_repo_name( + "git@git.code.tiararodney.com:h5p-mirror/foo.git" + ) == "foo" + + def test_without_git_suffix(self) -> None: + assert parse_repo_name( + "git@git.code.tiararodney.com:h5p-mirror/foo" + ) == "foo" + + def test_rejects_https_url(self) -> None: + with pytest.raises(ValueError): + parse_repo_name("https://git.code.tiararodney.com/x/foo.git") + + def test_rejects_url_without_colon(self) -> None: + with pytest.raises(ValueError): + parse_repo_name("git.code.tiararodney.com/x/foo.git")