Skip to content
1 change: 1 addition & 0 deletions src/vcspull/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
from logging import NullHandler

from . import cli
from .url import enable_ssh_style_url_detection # Import custom URL handling

logging.getLogger(__name__).addHandler(NullHandler())
3 changes: 3 additions & 0 deletions src/vcspull/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from vcspull import exc
from vcspull.config import filter_repos, find_config_files, load_configs
from vcspull.url import enable_ssh_style_url_detection

if t.TYPE_CHECKING:
import argparse
Expand Down Expand Up @@ -147,6 +148,8 @@ def update_repo(
# repo_dict: Dict[str, Union[str, Dict[str, GitRemote], pathlib.Path]]
) -> GitSync:
"""Synchronize a single repository."""
# Ensure SSH-style URLs are recognized as explicit Git URLs
enable_ssh_style_url_detection()
repo_dict = deepcopy(repo_dict)
if "pip_url" not in repo_dict:
repo_dict["pip_url"] = repo_dict.pop("url")
Expand Down
113 changes: 113 additions & 0 deletions src/vcspull/url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""URL handling for vcspull."""

from __future__ import annotations

from typing import Any

from libvcs.url.git import DEFAULT_RULES

_orig_rule_meta: dict[str, tuple[bool, int]] = {}


def enable_ssh_style_url_detection() -> None:
"""Enable detection of SSH-style URLs as explicit Git URLs.

This makes the core-git-scp rule explicit, which allows URLs like
'user@hostname:path/to/repo.git' to be detected with is_explicit=True.

Examples
--------
>>> from vcspull.url import enable_ssh_style_url_detection
>>> from libvcs.url.git import GitURL
>>> # Without the patch
>>> GitURL.is_valid('user@hostname:path/to/repo.git', is_explicit=True)
False
>>> # With the patch
>>> enable_ssh_style_url_detection()
>>> GitURL.is_valid('user@hostname:path/to/repo.git', is_explicit=True)
True
"""
# Patch the core-git-scp rule, storing its original state if not already stored
for rule in DEFAULT_RULES:
if rule.label == "core-git-scp":
if rule.label not in _orig_rule_meta:
_orig_rule_meta[rule.label] = (rule.is_explicit, rule.weight)
rule.is_explicit = True
rule.weight = 100
break


def disable_ssh_style_url_detection() -> None:
"""Disable detection of SSH-style URLs as explicit Git URLs.

This reverts the core-git-scp rule to its original state, where URLs like
'user@hostname:path/to/repo.git' are not detected with is_explicit=True.

Examples
--------
>>> from vcspull.url import enable_ssh_style_url_detection
>>> from vcspull.url import disable_ssh_style_url_detection
>>> from libvcs.url.git import GitURL
>>> # Enable the patch
>>> enable_ssh_style_url_detection()
>>> GitURL.is_valid('user@hostname:path/to/repo.git', is_explicit=True)
True
>>> # Disable the patch
>>> disable_ssh_style_url_detection()
>>> GitURL.is_valid('user@hostname:path/to/repo.git', is_explicit=True)
False
"""
# Restore the core-git-scp rule to its original state, if known
for rule in DEFAULT_RULES:
if rule.label == "core-git-scp":
orig = _orig_rule_meta.get(rule.label)
if orig:
rule.is_explicit, rule.weight = orig
_orig_rule_meta.pop(rule.label, None)
else:
# Fallback to safe defaults
rule.is_explicit = False
rule.weight = 0
break


def is_ssh_style_url_detection_enabled() -> bool:
"""Check if SSH-style URL detection is enabled.

Returns
-------
bool: True if SSH-style URL detection is enabled, False otherwise.
"""
for rule in DEFAULT_RULES:
if rule.label == "core-git-scp":
return rule.is_explicit
return False


"""
Context manager and utility for SSH-style URL detection.
"""


class ssh_style_url_detection:
"""Context manager to enable/disable SSH-style URL detection."""

def __init__(self, enabled: bool = True) -> None:
self.enabled = enabled

def __enter__(self) -> None:
"""Enable or disable SSH-style URL detection on context enter."""
if self.enabled:
enable_ssh_style_url_detection()
else:
disable_ssh_style_url_detection()

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: Any,
) -> None:
"""Restore original SSH-style URL detection state on context exit."""
# Always restore to disabled after context
disable_ssh_style_url_detection()
29 changes: 29 additions & 0 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ class ConfigVariationTest(t.NamedTuple):
""",
remote_list=["git_scheme_repo"],
),
ConfigVariationTest(
test_id="expanded_repo_style_with_unprefixed_remote_3",
config_tpl="""
{tmp_path}/study/myrepo:
{CLONE_NAME}:
repo: git+file://{path}
remotes:
git_scheme_repo: [email protected]:org/repo.git
""",
remote_list=["git_scheme_repo"],
),
ConfigVariationTest(
test_id="expanded_repo_style_with_unprefixed_repo",
config_tpl="""
{tmp_path}/study/myrepo:
{CLONE_NAME}:
repo: [email protected]:org/repo.git
""",
remote_list=["git_scheme_repo"],
),
ConfigVariationTest(
test_id="expanded_repo_style_with_prefixed_repo_3_with_prefix",
config_tpl="""
{tmp_path}/study/myrepo:
{CLONE_NAME}:
repo: git+ssh://[email protected]:org/repo.git
""",
remote_list=["git_scheme_repo"],
),
]


Expand Down
141 changes: 141 additions & 0 deletions tests/test_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Tests for URL handling in vcspull."""

from __future__ import annotations

import pytest
from libvcs.url.git import DEFAULT_RULES, GitURL

from vcspull.url import (
disable_ssh_style_url_detection,
enable_ssh_style_url_detection,
ssh_style_url_detection,
)


def test_ssh_style_url_detection_toggle() -> None:
"""Test that SSH-style URL detection can be toggled on and off."""
url = "[email protected]:org/repo.git"

# First, disable the detection
disable_ssh_style_url_detection()

# Without the patch, SSH-style URLs should not be detected as explicit
assert GitURL.is_valid(url) # Should be valid in non-explicit mode
assert not GitURL.is_valid(
url, is_explicit=True
) # Should not be valid in explicit mode

# Now enable the detection
enable_ssh_style_url_detection()

# With the patch, SSH-style URLs should be detected as explicit
assert GitURL.is_valid(url) # Should still be valid in non-explicit mode
assert GitURL.is_valid(
url, is_explicit=True
) # Should now be valid in explicit mode

# Verify the rule used
git_url = GitURL(url)
assert git_url.rule == "core-git-scp"

# Re-enable for other tests
enable_ssh_style_url_detection()


@pytest.mark.parametrize(
"url",
[
"[email protected]:org/repo.git",
"[email protected]:vcs-python/vcspull.git",
"[email protected]:vcs-python/vcspull.git",
"[email protected]:path/to/repo.git",
],
)
def test_ssh_style_url_detection(url: str) -> None:
"""Test that SSH-style URLs are correctly detected."""
# Ensure detection is enabled
enable_ssh_style_url_detection()

assert GitURL.is_valid(url)
assert GitURL.is_valid(url, is_explicit=True) # Should be valid in explicit mode
git_url = GitURL(url)
assert git_url.rule == "core-git-scp"


@pytest.mark.parametrize(
"url,expected_user,expected_hostname,expected_path",
[
(
"[email protected]:org/repo.git",
"user",
"myhostname.de",
"org/repo",
),
(
"[email protected]:vcs-python/vcspull.git",
"git",
"github.com",
"vcs-python/vcspull",
),
(
"[email protected]:vcs-python/vcspull.git",
"git",
"gitlab.com",
"vcs-python/vcspull",
),
(
"[email protected]:path/to/repo.git",
"user",
"custom-host.com",
"path/to/repo",
),
],
)
def test_ssh_style_url_parsing(
url: str, expected_user: str, expected_hostname: str, expected_path: str
) -> None:
"""Test that SSH-style URLs are correctly parsed."""
# Ensure detection is enabled
enable_ssh_style_url_detection()

git_url = GitURL(url)
assert git_url.user == expected_user
assert git_url.hostname == expected_hostname
assert git_url.path == expected_path
assert git_url.suffix == ".git"


def test_enable_disable_restores_original_state() -> None:
"""Original rule metadata is preserved and restored after enable/disable."""
# Ensure any prior patch is cleared
disable_ssh_style_url_detection()
# Find the core-git-scp rule and capture its original state
rule = next(r for r in DEFAULT_RULES if r.label == "core-git-scp")
orig_state = (rule.is_explicit, rule.weight)

# Disabling without prior enable should leave original state
disable_ssh_style_url_detection()
assert (rule.is_explicit, rule.weight) == orig_state

# Enable should patch
enable_ssh_style_url_detection()
assert rule.is_explicit is True
assert rule.weight == 100

# Disable should restore to original
disable_ssh_style_url_detection()
assert (rule.is_explicit, rule.weight) == orig_state


def test_context_manager_restores_original_state() -> None:
"""Context manager enables then restores original rule state."""
rule = next(r for r in DEFAULT_RULES if r.label == "core-git-scp")
orig_state = (rule.is_explicit, rule.weight)

# Use context manager
with ssh_style_url_detection():
assert rule.is_explicit is True
assert rule.weight == 100

# After context, state should be back to original
assert (rule.is_explicit, rule.weight) == orig_state
Loading