Repository navigation
fix(git): report a detached HEAD from git_checkout (#4804) - #5023
Conversation
git_checkout validated branch_name with rev_parse, which resolves any revision (a sha, tag, HEAD~1, or a remote-tracking ref), then always replied "Switched to branch '<name>'". For anything that is not a branch git actually detached HEAD, so the tool handed the model a success sentence over a state where new commits belong to no branch. Report the resulting state instead: say HEAD is detached, with the short sha, when it is; keep the branch wording when a branch was really checked out. Signed-off-by: cliffhall <cliff@futurescale.com>
…a branch Adds the parametrized case suggested in review: HEAD~1, refs/heads/feature and origin/main are the revisions a caller is most likely to send and none of them is a branch, so none may be reported as a branch switch. All three fail against the old unconditional return and pass with the fix. Signed-off-by: cliffhall <cliff@futurescale.com>
Replace the KNOWN BUG #4804 pin in test_protocol.py: a non-branch revision now gets "HEAD is now detached at <short sha>", and the detached commit is the one the revision names. Document the detached reply in the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation matches the issue requirements and is covered by focused unit and protocol tests.
Review effort: Balanced
Findings: None
What changed in this PR
Fixes git_checkout to accurately report detached HEAD states while preserving support for non-branch revisions.
Changes:
- Reports detached HEAD with the checked-out short SHA.
- Adds unit and protocol regression coverage.
- Documents the updated response behavior.
| File | Description |
|---|---|
src/git/src/mcp_server_git/server.py |
Reports actual post-checkout state. |
src/git/tests/test_server.py |
Tests branches and non-branch revisions. |
src/git/tests/test_protocol.py |
Updates the protocol-level bug pin. |
src/git/README.md |
Documents detached HEAD responses. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
|
Copilot round 1: clean (no findings, no suppressed comments). The review loop ends on the clean-round exit. |
Closes #4804
Part of Wave 6 (
git) of #5004.Description
git_checkoutvalidatesbranch_namewithrepo.rev_parse(), which accepts any revision (a sha, a tag,HEAD~1,refs/heads/<x>, a remote-tracking ref). For anything that is not a branch,git checkoutdetaches HEAD, yet the tool always repliedSwitched to branch '<name>'. The reply is the only thing the calling model sees, so it was told it was on a branch when it was not.After the checkout the tool now reads the resulting state:
Non-branch revisions are still accepted (rejecting them would remove a capability callers use on purpose); the reply is just truthful now.
This ports candidate PR #4805 by @CryoThrust, cherry-picked onto
v2/mainwith authorship kept (commitsfix(git): report a detached HEAD from git_checkoutandtest(git): pin non-branch checkout revisions ...). The only change from #4805 in those commits is conflict resolution plusruff formatintests/test_server.py. A third commit replaces the Wave 1 pin.Server Details
git(src/git,mcp-server-git, legacy era,mcp>=1,<2)git_checkouttool's reply (server.py);tests/test_server.py(cases from fix(git): report a detached HEAD from git_checkout #4805);tests/test_protocol.py(theKNOWN BUG #4804pin now asserts the correct reply and that HEAD is detached at the commit the revision names; marker removed); README'sgit_checkout"Returns" line.Motivation and Context
#4804. A detached HEAD reported as a branch switch leads an agent to commit onto no branch, and that work is reachable only through the reflog.
git grep "KNOWN BUG #4804"now returns nothing.How Has This Been Tested?
Unit / protocol tests:
uv run pytestinsrc/git: 145 passed; per-file branch coverageserver.py100%.ruff check,ruff format --check,pyright: clean.Gate:
npm run local:gateexited 0 (all stages, including the per-file coverage gate: fetch, git, time PASS; boot smoke 9/9).Inspector CLI (
@modelcontextprotocol/inspector@2.9.0 --cli, legacy era, 2025-11-25). Fixture repo: commits c1, c2 (113299d, taggedv1), c3 (cdd4d1a, onmainandfeature). Each row resets tomain, then calls:Before (
v2/main'sserver.py):After (this branch):
2026-07-28 era: not available. The server is still on
mcp1.x and does not implementserver/discover, so--protocol-era modernfails as expected for an unmigrated server:Version negotiation failed: the server did not offer pinned protocol version 2026-07-28 via server/discover (no fallback in pin mode)(exit 1). Modern-era support arrives with #4853.LLM client: Claude Code 2.1.289, headless (
claude -p --mcp-config <local build> --strict-mcp-config --allowedTools mcp__git__git_checkout). The era cannot be chosen from this client; against a v1-SDK server the connection is legacy. Prompt: "Call the git_checkout tool of the git server with repo_path '' and branch_name 'v1'. Then reply with exactly the text the tool returned, and on a second line say in a few words whether you are now on a branch."Switched to branch 'v1'/ "No, I'm not on a branch:v1is a tag, so HEAD is now detached at 113299da, even though the tool says "branch"." (The model had to second-guess the tool's reply from the name; on the first attempt it ran out of turns trying to verify.)HEAD is now detached at 113299d/ "No, I'm not on a branch. HEAD is detached."Breaking Changes
None for client configuration. The text of
git_checkout's reply changes for non-branch revisions only.Types of changes
Checklist
npm run changeset) if this changes what a TypeScript server publishes (not applicable: Python server)BadNamehandling is unchanged)Additional context
Credit to @CryoThrust for the fix and tests in #4805, which this ports; that PR targets
mainand is reference only.🤖 Generated with Claude Code