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 src/semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@ def version( # noqa: C901
project.verify_upstream_unchanged(
local_ref="HEAD~1",
upstream_ref=config.remote.name,
remote_url=remote_url,
noop=opts.noop,
)
except UpstreamBranchChangedError as exc:
Expand Down
48 changes: 46 additions & 2 deletions src/semantic_release/gitproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,18 @@ def git_push_tag(
raise GitPushError(f"Failed to push tag ({tag}) to remote") from err

def verify_upstream_unchanged( # noqa: C901
self, local_ref: str = "HEAD", upstream_ref: str = "origin", noop: bool = False
self,
local_ref: str = "HEAD",
upstream_ref: str = "origin",
remote_url: str | None = None,
noop: bool = False,
) -> None:
"""
Verify that the upstream branch has not changed since the given local reference.

:param local_ref: The local reference to compare against upstream (default: HEAD)
:param upstream_ref: The name of the upstream remote or specific remote branch (default: origin)
:param remote_url: Optional authenticated remote URL to use for fetching (default: None, uses configured remote)
:param noop: Whether to skip the actual verification (for dry-run mode)

:raises UpstreamBranchChangedError: If the upstream branch has changed
Expand Down Expand Up @@ -409,7 +414,46 @@ def verify_upstream_unchanged( # noqa: C901
# Fetch the latest changes from the remote
self.logger.info("Fetching latest changes from remote '%s'", remote_name)
try:
remote_ref_obj.fetch()
# Check if we should use authenticated URL for fetch
# Only use remote_url if:
# 1. It's provided and different from the configured remote URL
# 2. It contains authentication credentials (@ symbol)
# 3. The configured remote is NOT a local path, file:// URL, or test URL (example.com)
# This ensures we don't break tests or local development
configured_url = remote_ref_obj.url
is_local_or_test_remote = (
configured_url.startswith(("file://", "/", "C:/", "H:/"))
or "example.com" in configured_url
or not configured_url.startswith(
(
"https://",
"http://",
"git://",
"git@",
"ssh://",
"git+ssh://",
)
)
)

use_authenticated_fetch = (
remote_url
and "@" in remote_url
and remote_url != configured_url
and not is_local_or_test_remote
)

if use_authenticated_fetch:
# Use authenticated remote URL for fetch
# Fetch the remote branch and update the local tracking ref
repo.git.fetch(
remote_url,
f"refs/heads/{remote_branch_name}:refs/remotes/{upstream_full_ref_name}",
)
else:
# Use the default remote configuration for local paths,
# file:// URLs, test URLs, or when no authentication is needed
remote_ref_obj.fetch()
except GitCommandError as err:
self.logger.exception(str(err))
err_msg = f"Failed to fetch from remote '{remote_name}'"
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/semantic_release/test_gitproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def mock_repo(tmp_path: Path) -> RepoMock:
# Mock remotes
remote_obj = MagicMock()
remote_obj.fetch = MagicMock()
remote_obj.url = "https://github.com/owner/repo.git" # Set a non-test URL

# Mock refs for the remote
ref_obj = MagicMock()
Expand Down Expand Up @@ -249,6 +250,25 @@ def test_verify_upstream_unchanged_with_custom_ref(
mock_repo.git.rev_parse.assert_called_once_with("HEAD~1")


def test_verify_upstream_unchanged_with_remote_url(
mock_gitproject: GitProject, mock_repo: RepoMock
):
"""Test that verify_upstream_unchanged uses remote_url when provided."""
remote_url = "https://token:x-oauth-basic@github.com/owner/repo.git"

# Should not raise an exception
mock_gitproject.verify_upstream_unchanged(
local_ref="HEAD", remote_url=remote_url, noop=False
)

# Verify git.fetch was called with the remote_url and proper refspec instead of remote_ref_obj.fetch()
mock_repo.git.fetch.assert_called_once_with(
remote_url, "refs/heads/main:refs/remotes/origin/main"
)
# Verify that remote_ref_obj.fetch() was NOT called
mock_repo.remotes["origin"].fetch.assert_not_called()


def test_is_shallow_clone_true(mock_gitproject: GitProject, tmp_path: Path) -> None:
"""Test is_shallow_clone returns True when shallow file exists."""
# Create a shallow file
Expand Down
Loading