Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Bare-cache git commands now use `--git-dir` instead of `-C` for compatibility with git 2.53.0 (`safe.bareRepository=explicit` default), and fetched SHAs are pinned as synthetic refs so `git clone --local --shared` includes them in the object transfer. (#1267)
- `apm install` no longer silently overwrites pre-existing governance files; `check_collision()` now treats `managed_files=None` (first install, no lockfile yet) as an empty set so hand-rolled files in `.github/instructions/` and other governance directories are correctly detected and protected from silent overwrite. (#1256)
- `test_find_server_by_reference_uuid_not_found` no longer leaks a live HTTP call to `api.mcp.github.com` (fixing Windows CI failures from `socket.gaierror`); the `search_servers` fallback is now mocked. (#1264)
- ADO full HTTPS URLs with sub-path virtual packages (e.g. `https://dev.azure.com/org/proj/_git/repo/sub/path`) are now parsed correctly instead of being rejected. (#1254)
Expand Down
56 changes: 48 additions & 8 deletions src/apm_cli/deps/bare_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _scrub_bare_remote_url(bare_path: Path, git_exe: str, env: dict[str, str]) -
"""
try:
result = subprocess.run(
[git_exe, "-C", str(bare_path), "remote", "set-url", "origin", "redacted://"],
[git_exe, "--git-dir", str(bare_path), "remote", "set-url", "origin", "redacted://"],
env=env,
check=False,
capture_output=True,
Expand Down Expand Up @@ -197,13 +197,13 @@ def _bare_action(url: str, env: dict[str, str], target: Path) -> None:
capture_output=True,
)
subprocess.run(
[git_exe, "-C", str(target), "remote", "add", "origin", url],
[git_exe, "--git-dir", str(target), "remote", "add", "origin", url],
env=env,
check=True,
capture_output=True,
)
subprocess.run(
[git_exe, "-C", str(target), "fetch", "--depth=1", "origin", ref],
[git_exe, "--git-dir", str(target), "fetch", "--depth=1", "origin", ref],
env=env,
check=True,
capture_output=True,
Expand All @@ -214,7 +214,7 @@ def _bare_action(url: str, env: dict[str, str], target: Path) -> None:
# Without update-ref, consumer's `git rev-parse HEAD`
# is ambiguous. See 6.18.
subprocess.run(
[git_exe, "-C", str(target), "update-ref", "HEAD", ref],
[git_exe, "--git-dir", str(target), "update-ref", "HEAD", ref],
env=env,
check=True,
capture_output=True,
Expand Down Expand Up @@ -244,7 +244,7 @@ def _bare_action(url: str, env: dict[str, str], target: Path) -> None:
full_sha_result = subprocess.run(
[
git_exe,
"-C",
"--git-dir",
str(target),
"rev-parse",
"--verify",
Expand All @@ -257,7 +257,7 @@ def _bare_action(url: str, env: dict[str, str], target: Path) -> None:
)
full_sha = full_sha_result.stdout.strip()
subprocess.run(
[git_exe, "-C", str(target), "update-ref", "HEAD", full_sha],
[git_exe, "--git-dir", str(target), "update-ref", "HEAD", full_sha],
env=env,
check=True,
capture_output=True,
Expand Down Expand Up @@ -367,6 +367,43 @@ def _rev_parse_present() -> bool:
except Exception:
return False

def _pin_sha_as_head_ref() -> None:
"""Add refs/heads/apm-pin-<sha12> so the SHA is reachable via git-clone.

``git clone --local --shared`` from a *shallow* bare ignores
``--shared`` and falls back to the upload-pack protocol, which
only transfers objects reachable from advertised refs. A SHA
fetched by :func:`fetch_sha_into_bare` is inserted into the
object store but is *not* referenced by any ref, so the clone
silently omits it and subsequent ``git checkout <sha>`` fails.

Creating a synthetic ``refs/heads/apm-pin-*`` ref makes the
commit reachable via the default ``refs/heads/*`` refspec, so
upload-pack includes it. Best-effort: a failure here is logged
at DEBUG level and does not abort the install (the fallback is
a fresh bare clone for the pinned package).
"""
ref_name = f"refs/heads/apm-pin-{sha[:12]}"
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
Outdated
try:
subprocess.run(
[git_exe, "--git-dir", str(bare_path), "update-ref", ref_name, sha],
capture_output=True,
timeout=10,
)
_log.debug(
"fetch_sha_into_bare: pinned %s as %s in %s",
sha[:12],
ref_name,
bare_path,
)
except Exception as exc:
_log.debug(
"fetch_sha_into_bare: could not create pin ref for %s in %s: %s",
sha[:12],
bare_path,
exc,
)
Comment thread
sergio-sisternes-epam marked this conversation as resolved.

def _scrub_fetch_head() -> None:
"""Truncate FETCH_HEAD to remove the token-embedded URL written by fetch."""
fetch_head = bare_path / "FETCH_HEAD"
Expand All @@ -385,6 +422,7 @@ def _scrub_fetch_head() -> None:
_log.debug("fetch_sha_into_bare: checking if %s is present in %s", sha[:12], bare_path)
if _rev_parse_present():
_log.debug("fetch_sha_into_bare: SHA %s already present, skipping fetch", sha[:12])
_pin_sha_as_head_ref()
return True

# Step 2: shallow fetch by full SHA (only for full 40-char SHAs).
Expand All @@ -395,7 +433,7 @@ def _scrub_fetch_head() -> None:

def _fetch_action_sha(url: str, env: dict[str, str], target: Path) -> None:
subprocess.run(
[git_exe, "-C", str(bare_path), "fetch", "--depth=1", url, sha],
[git_exe, "--git-dir", str(bare_path), "fetch", "--depth=1", url, sha],
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
Outdated
env=env,
check=True,
capture_output=True,
Expand All @@ -412,6 +450,7 @@ def _fetch_action_sha(url: str, env: dict[str, str], target: Path) -> None:
_scrub_fetch_head()
if _rev_parse_present():
_log.debug("fetch_sha_into_bare: shallow fetch of %s succeeded", sha[:12])
_pin_sha_as_head_ref()
return True
except subprocess.CalledProcessError as exc:
stderr_text = ""
Expand All @@ -437,7 +476,7 @@ def _fetch_action_sha(url: str, env: dict[str, str], target: Path) -> None:

def _fetch_action_broad(url: str, env: dict[str, str], target: Path) -> None:
subprocess.run(
[git_exe, "-C", str(bare_path), "fetch", f"--depth={broad_depth}", url],
[git_exe, "--git-dir", str(bare_path), "fetch", f"--depth={broad_depth}", url],
Comment thread
sergio-sisternes-epam marked this conversation as resolved.
Outdated
env=env,
check=True,
capture_output=True,
Expand All @@ -454,6 +493,7 @@ def _fetch_action_broad(url: str, env: dict[str, str], target: Path) -> None:
_scrub_fetch_head()
if _rev_parse_present():
_log.debug("fetch_sha_into_bare: broad fetch succeeded, %s now present", sha[:12])
_pin_sha_as_head_ref()
return True
except subprocess.CalledProcessError as exc:
stderr_text = ""
Expand Down
10 changes: 8 additions & 2 deletions tests/unit/deps/test_shared_clone_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,8 +1145,11 @@ def test_sha_already_present_returns_true_without_fetch(self, tmp_path: Path) ->
assert result is True
# execute_transport_plan should NOT be called
mock_execute.assert_not_called()
# Only one rev-parse call (the initial check)
assert mock_run.call_count == 1
# Two subprocess.run calls: rev-parse (check) + update-ref (pin ref)
assert mock_run.call_count == 2
pin_call_argv = mock_run.call_args_list[1][0][0]
assert "update-ref" in pin_call_argv
assert f"refs/heads/apm-pin-{sha[:12]}" in pin_call_argv

def test_shallow_fetch_full_sha_succeeds(self, tmp_path: Path) -> None:
"""Full 40-char SHA: shallow fetch via transport plan succeeds."""
Expand Down Expand Up @@ -1175,6 +1178,7 @@ def mock_execute(
mock_run.side_effect = [
MagicMock(returncode=1), # SHA not present initially
MagicMock(returncode=0), # SHA present after fetch
MagicMock(returncode=0), # update-ref pin (apm-pin-<sha12>)
]

result = fetch_sha_into_bare(
Expand Down Expand Up @@ -1226,6 +1230,7 @@ def mock_execute(
mock_run.side_effect = [
MagicMock(returncode=1), # SHA not present
MagicMock(returncode=0), # SHA present after broad fetch
MagicMock(returncode=0), # update-ref pin (apm-pin-<sha12>)
]

result = fetch_sha_into_bare(
Expand Down Expand Up @@ -1306,6 +1311,7 @@ def mock_execute(
mock_run.side_effect = [
MagicMock(returncode=1), # SHA not present
MagicMock(returncode=0), # SHA present after fetch
MagicMock(returncode=0), # update-ref pin (apm-pin-<sha12>)
]

fetch_sha_into_bare(
Expand Down
Loading