fix: treat ref=HEAD on empty repo as empty tree#3158
Conversation
|
✅ PR Artifacts Cleaned Up The |
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
all-hands-bot
left a comment
There was a problem hiding this comment.
Clean bug fix that mirrors existing empty-repo handling pattern. Appropriately surgical - only intercepts the well-known HEAD case while letting other bad refs fail diagnostically.
Risk Assessment: 🟢 LOW - Edge case fix with good test coverage, no agent behavior changes.
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
Verified the fix resolves the HTTP 400 error when opening the Changes tab on a fresh conversation with no commits.
Does this PR achieve its stated goal?
Yes. The PR successfully fixes the HTTP 400 error that occurred when requesting GET /api/git/changes?ref=HEAD on a freshly-initialized git repo with no commits. The fix correctly handles the empty-repo edge case by returning GIT_EMPTY_TREE_HASH, which allows untracked files to render as ADDED instead of raising a GitCommandError. I verified this by reproducing the bug on the main branch and confirming it's fixed on the PR branch, testing both the SDK layer and the API router layer.
| Phase | Result |
|---|---|
| Environment Setup | ✅ Dependencies synced with uv sync --frozen |
| CI Status | ✅ Key checks passing: Python API, REST API, pre-commit, agent-server-tests, sdk-tests (pending), tools-tests, workspace-tests |
| Functional Verification | ✅ Reproduced bug, verified fix, tested edge cases |
Functional Verification
Test 1: Reproduce the bug (main branch, without fix)
Step 1 — Reproduce the bug without the fix:
Checked out origin/main and ran a test that simulates the exact scenario: freshly-initialized git repo, no commits, request ref=HEAD.
$ git checkout origin/main
$ uv run python /tmp/test_git_changes.pyOutput:
=== Testing in: /tmp/tmpmee1w_cm ===
Attempting to get git changes with ref=HEAD...
{"levelname": "ERROR", "message": "Git command failed: git --no-pager rev-parse --verify 'HEAD^{commit}'. Exit code: 128. Stderr: fatal: Needed a single revision\n"}
❌ FAILED: GitCommandError raised: Git command failed: git --no-pager rev-parse --verify 'HEAD^{commit}'
Interpretation: This confirms the bug exists. When ref=HEAD is requested on an empty repo, the SDK tries to resolve HEAD^{commit} which fails because HEAD doesn't exist yet. This error bubbles up as HTTP 400 in the agent-server router.
Step 2 — Apply the PR's changes:
Checked out the PR branch hieptl/app-1630.
$ git checkout hieptl/app-1630Step 3 — Re-run with the fix in place:
$ uv run python /tmp/test_git_changes.pyOutput:
=== Testing in: /tmp/tmpeh5i6aok ===
Attempting to get git changes with ref=HEAD...
{"levelname": "INFO", "message": "Found 1 total git changes in /tmp/tmpeh5i6aok"}
✅ SUCCESS: Got changes: [GitChange(status=<GitChangeStatus.ADDED: 'ADDED'>, path=PosixPath('untracked.txt'))]
Interpretation: The fix works. The get_valid_ref() function now intercepts ref=HEAD on empty repos and returns GIT_EMPTY_TREE_HASH, which allows the git diff to succeed. Untracked files correctly appear as ADDED status.
Test 2: SDK test passes
Ran the new test test_get_changes_in_repo_ref_head_on_empty_repo_returns_untracked_as_added:
$ uv run pytest tests/sdk/git/test_git_changes.py::test_get_changes_in_repo_ref_head_on_empty_repo_returns_untracked_as_added -vResult: PASSED in 0.28s
Test 3: API router test passes
Ran the new test test_git_changes_query_param_ref_head_on_empty_repo_returns_200:
$ uv run pytest tests/agent_server/test_git_router.py::test_git_changes_query_param_ref_head_on_empty_repo_returns_200 -vResult: PASSED in 1.09s
This end-to-end test verifies that GET /api/git/changes?path=<empty-repo>&ref=HEAD returns HTTP 200 with the correct JSON response:
[{"status": "ADDED", "path": "untracked.txt"}]Test 4: Edge cases verified
Ran additional tests to ensure the fix doesn't break other scenarios:
- Normal repo with commits, ref=HEAD: ✅ Still works correctly
- Empty repo with invalid ref (e.g.,
nonexistent-branch): ✅ Still raisesGitCommandError(correct behavior for bad refs) - Empty repo without explicit ref parameter: ✅ Works correctly (uses auto-detected ref)
Output:
=== Test 1: Normal repo with commits, ref=HEAD ===
✅ SUCCESS: Got changes: [GitChange(status=<GitChangeStatus.ADDED: 'ADDED'>, path=PosixPath('untracked.txt'))]
=== Test 2: Empty repo with invalid ref ===
✅ SUCCESS: Correctly raised GitCommandError: Git command failed: git --no-pager rev-parse --verify 'nonexistent-branch^{commit}'
=== Test 3: Empty repo without ref parameter ===
✅ SUCCESS: Got changes: [GitChange(status=<GitChangeStatus.ADDED: 'ADDED'>, path=PosixPath('untracked.txt'))]
=== Summary: 3/3 tests passed ===
Interpretation: The fix is surgical and correctly scoped. It only intercepts the specific case of ref="HEAD" on empty repos. All other scenarios continue to work as before, including proper error handling for genuinely invalid refs.
Issues Found
None.
Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
Why
Problem
When a user clicks Create New Conversation in
agent-server-gui(localhost:3001) and opens the Changes tab, the tab renders the raw shell command in a red error banner:Reproduction
http://127.0.0.1:8000) and the GUI dev server (http://localhost:3001).The failing request:
Root cause
The runtime
git inits the new workspace, but does not commit anything — soHEADdoes not resolve. The Changes tab requests?ref=HEADto get git-status semantics (agent-server-gui/src/api/git-service/agent-server-git-service.api.ts:74). On the backend, the SDK'sget_valid_ref()(software-agent-sdk/openhands-sdk/openhands/sdk/git/utils.py) runsgit rev-parse --verify 'HEAD^{commit}'unconditionally on the override branch, which exits non-zero.run_git_commandraisesGitCommandError→ the router translates that to HTTP 400.The default-ref branch of the same function already handles empty repos via
_repo_has_commits()+GIT_EMPTY_TREE_HASH. The override branch was simply missing that check.Expected behavior
Opening the Changes tab on a brand-new conversation should render the empty/no-changes state (or any untracked files as added) with no error banner.
Acceptance criteria
GET /api/git/changes?path=<empty-repo>&ref=HEADreturns 200 with a list (empty or untracked-as-ADDED).GET /api/git/changes?path=<repo-with-commits>&ref=HEADkeeps existing git-status semantics (working tree vs HEAD).GET /api/git/changes?path=<repo>&ref=<bad-ref>still returns 400 withGitCommandErrorso genuine bad refs stay diagnosable.git rev-parse --verify 'HEAD^{commit}'banner on a fresh conversation.ref=HEADcase at both the SDK and router layers.Summary
/api/git/changes?ref=HEADwhen the workspace is a freshly-init'd git repo with no commits yet.GIT_EMPTY_TREE_HASH, so untracked files render asADDED.if-block inget_valid_ref(). No router changes, no signature changes, no frontend changes.Symptom
The Changes tab in
agent-server-guishows this banner the first time you open a fresh conversation:Root cause
The override branch never checked
_repo_has_commits(). The Changes tab passesref: "HEAD"deliberately (git-status semantics), so every brand-new conversation hit the failing path.Fix
Narrow on purpose: only canonical
HEADis intercepted. A bad commit hash, a typoed branch name, or any other override still raisesGitCommandError→ HTTP 400, so genuine bad refs stay diagnostic.Why not fix the frontend instead?
agent-server-guipassesref: "HEAD"deliberately to get git-status semantics (working tree vs HEAD). When the workspace has commits, that request is correct and useful. The bug is the backend's failure to handle the "no HEAD yet" edge case — fixing it at the source also covers any other client (curl, future SDKs) that passes?ref=HEAD.Why not catch
GitCommandErrorin the router?That would mask real failures (bad ref, broken repo) as "no changes" and lose the diagnostic value of the 400 path. The fix targets the one well-known empty-repo case, not blanket-suppress git errors.
How to Test
http://127.0.0.1:8000) and the GUI dev server (http://localhost:3001).Demo Video/Screenshots
Type
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:8955993-pythonRun
All tags pushed for this build
About Multi-Architecture Support
8955993-python) is a multi-arch manifest supporting both amd64 and arm648955993-python-amd64) are also available if needed