Skip to content

fix(git): treat ref=HEAD with unresolved HEAD as empty tree#3177

Open
rbren wants to merge 2 commits intomainfrom
fix/git-changes-empty-repo-explicit-ref
Open

fix(git): treat ref=HEAD with unresolved HEAD as empty tree#3177
rbren wants to merge 2 commits intomainfrom
fix/git-changes-empty-repo-explicit-ref

Conversation

@rbren
Copy link
Copy Markdown
Contributor

@rbren rbren commented May 9, 2026

The empty-repo fallback in get_valid_ref relied on _repo_has_commits to decide whether to short-circuit ref="HEAD" to the empty tree. That check returns True for repos that have any commits anywhere — so an orphan branch (HEAD pointing to a ref with no commit, while other branches do have commits) still ran git rev-parse --verify HEAD^{commit} and failed with:

HTTP 400 Bad Request
{"detail":"Git command failed: git --no-pager rev-parse --verify
'HEAD^{commit}'"}

surfacing in the agent-canvas Changes tab whenever a workspace was in that state.

Replace the pre-check with a tighter try/except around the rev-parse itself: when override == "HEAD" and rev-parse fails, fall back to GIT_EMPTY_TREE_HASH (empty repo or orphan branch — anything where HEAD has no commit yet). Non-"HEAD" overrides still raise so typo'd branches/SHAs aren't silently turned into "no changes".

This also drops the extra git rev-list --count call on the ref="HEAD" happy path.

Tests:

  • New SDK regression: ref="HEAD" on an orphan branch returns untracked files as ADDED (mirrors existing empty-repo test).
  • New SDK regression: non-"HEAD" unresolved ref still raises GitCommandError.
  • New end-to-end router test: GET /api/git/changes?ref=HEAD on an orphan-branch repo returns 200 (was 400 before this fix).
  • A human has tested these changes.

Why

Summary

Issue Number

How to Test

Video/Screenshots

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:ca076f9-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-ca076f9-python \
  ghcr.io/openhands/agent-server:ca076f9-python

All tags pushed for this build

ghcr.io/openhands/agent-server:ca076f9-golang-amd64
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-golang-amd64
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-golang-amd64
ghcr.io/openhands/agent-server:ca076f9-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:ca076f9-golang-arm64
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-golang-arm64
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-golang-arm64
ghcr.io/openhands/agent-server:ca076f9-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:ca076f9-java-amd64
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-java-amd64
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-java-amd64
ghcr.io/openhands/agent-server:ca076f9-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:ca076f9-java-arm64
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-java-arm64
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-java-arm64
ghcr.io/openhands/agent-server:ca076f9-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:ca076f9-python-amd64
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-python-amd64
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-python-amd64
ghcr.io/openhands/agent-server:ca076f9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:ca076f9-python-arm64
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-python-arm64
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-python-arm64
ghcr.io/openhands/agent-server:ca076f9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:ca076f9-golang
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-golang
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-golang
ghcr.io/openhands/agent-server:ca076f9-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:ca076f9-java
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-java
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-java
ghcr.io/openhands/agent-server:ca076f9-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:ca076f9-python
ghcr.io/openhands/agent-server:ca076f9da9ad18169d6fce3438d7c06478057415-python
ghcr.io/openhands/agent-server:fix-git-changes-empty-repo-explicit-ref-python
ghcr.io/openhands/agent-server:ca076f9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., ca076f9-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., ca076f9-python-amd64) are also available if needed

The empty-repo fallback in get_valid_ref relied on _repo_has_commits to
decide whether to short-circuit ref="HEAD" to the empty tree. That check
returns True for repos that have *any* commits anywhere — so an orphan
branch (HEAD pointing to a ref with no commit, while other branches do
have commits) still ran git rev-parse --verify HEAD^{commit} and failed
with:

  HTTP 400 Bad Request
  {"detail":"Git command failed: git --no-pager rev-parse --verify
   'HEAD^{commit}'"}

surfacing in the agent-canvas Changes tab whenever a workspace was in
that state.

Replace the pre-check with a tighter try/except around the rev-parse
itself: when override == "HEAD" and rev-parse fails, fall back to
GIT_EMPTY_TREE_HASH (empty repo *or* orphan branch — anything where
HEAD has no commit yet). Non-"HEAD" overrides still raise so typo'd
branches/SHAs aren't silently turned into "no changes".

This also drops the extra git rev-list --count call on the ref="HEAD"
happy path.

Tests:
- New SDK regression: ref="HEAD" on an orphan branch returns untracked
  files as ADDED (mirrors existing empty-repo test).
- New SDK regression: non-"HEAD" unresolved ref still raises
  GitCommandError.
- New end-to-end router test: GET /api/git/changes?ref=HEAD on an
  orphan-branch repo returns 200 (was 400 before this fix).

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taste Rating: 🟢 Good taste

Clean bug fix that actually simplifies the code. Replacing the _repo_has_commits pre-check with direct exception handling eliminates the orphan-branch edge case without adding complexity. Comprehensive test coverage at both SDK and router levels.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW

Bug fix that makes existing git operations more robust. No API changes, no breaking changes. Affects read-only git inspection logic with excellent test coverage.

VERDICT:
Worth merging: Fixes real user-reported issue with cleaner implementation

KEY INSIGHT:
Elegant solution that eliminates a special case by handling the failure directly rather than trying to predict it.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/git
   utils.py1242778%72–74, 99–101, 189–190, 197–202, 207–208, 218–223, 233–235, 263, 373
TOTAL26472616976% 

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ QA Report: PASS

The PR successfully fixes the orphan branch bug and achieves its stated goal.

Does this PR achieve its stated goal?

Yes. The PR fixes the HTTP 400 error that occurred when calling /api/git/changes?ref=HEAD on an orphan branch. Before this fix, users saw GitCommandError: git --no-pager rev-parse --verify 'HEAD^{commit}' when HEAD pointed to an unborn branch. After this fix, the same scenario returns HTTP 200 with untracked files as ADDED changes. The fallback to empty-tree comparison is correctly limited to ref="HEAD" only, so typo'd branch names still raise errors as expected.

Phase Result
Environment Setup ✅ Dependencies installed, project builds successfully
CI Status ⚠️ Pre-commit failing (E731 style violation), all other checks passing or in progress
Functional Verification ✅ Bug reproduced, fix verified end-to-end, regression guards working
Functional Verification

Test 1: Reproduce the bug (BEFORE fix)

Setup: Created a git repo with commits on main, then switched to an orphan branch with unborn HEAD.

Command (parent commit 3a7a97a):

from openhands.sdk.git.git_changes import get_changes_in_repo
changes = get_changes_in_repo("/tmp/test_repo", ref="HEAD")

Result:

❌ GitCommandError: Git command failed: git --no-pager rev-parse --verify 'HEAD^{commit}'
   Exit code: 128
   Stderr: fatal: Needed a single revision

This confirms the bug existed: the old code tried to resolve HEAD, failed, and raised an exception that surfaced as HTTP 400 in the Changes tab.


Test 2: Verify the fix (AFTER fix)

Command (this PR commit 55f096c):

from openhands.sdk.git.git_changes import get_changes_in_repo
changes = get_changes_in_repo("/tmp/test_repo", ref="HEAD")

Result:

✅ SUCCESS: ref='HEAD' returned 2 changes
   Paths: {'committed.txt', 'untracked.txt'}

The fix works: get_valid_ref catches the GitCommandError, detects override == "HEAD", and falls back to GIT_EMPTY_TREE_HASH. Changes are returned successfully instead of raising an exception.


Test 3: Verify regression guard (non-HEAD refs still raise)

Command:

changes = get_changes_in_repo("/tmp/test_repo", ref="not-a-real-branch-name")

Result:

✅ GitCommandError raised as expected
   Error: Git command failed: git --no-pager rev-parse --verify 'not-a-real-branch-name^{commit}'

The fix correctly preserves strict validation for non-"HEAD" overrides. Typo'd branch names are not silently swallowed.


Test 4: Verify agent-server endpoint

Command:

GET /api/git/changes?path=/tmp/test_repo&ref=HEAD

Result:

HTTP 200 OK
[
  {"status": "ADDED", "path": "committed.txt"},
  {"status": "ADDED", "path": "untracked.txt"}
]

The end-to-end fix works through the HTTP API. Before this PR, the same request returned HTTP 400 with the git command error message.


Test 5: Verify new tests pass

All three new tests added in this PR pass:

✅ test_get_changes_in_repo_ref_head_on_orphan_branch_returns_untracked_as_added
✅ test_get_changes_in_repo_invalid_non_head_ref_still_raises_after_fix
✅ test_git_changes_query_param_ref_head_on_orphan_branch_returns_200

Issues Found

🟡 Minor: Pre-commit check failing due to E731 style violation in test file (lambda instead of def). See inline comment.

Comment thread tests/agent_server/test_git_router.py Outdated
@rbren
Copy link
Copy Markdown
Contributor Author

rbren commented May 9, 2026

@OpenHands fix the precommit

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 9, 2026

I'm on it! rbren can track my progress at all-hands.dev

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Copy Markdown
Contributor Author

rbren commented May 9, 2026

Fixed in ca076f9.

Validation:

  • uv run pre-commit run --files openhands-sdk/openhands/sdk/git/utils.py tests/sdk/git/test_git_changes.py tests/agent_server/test_git_router.py
  • uv run pytest tests/agent_server/test_git_router.py -k orphan_branch

This comment was created by an AI agent (OpenHands) on behalf of the user.

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 9, 2026

Since my last summary, there have been no additional code or PR changes.

Checklist:

  • Request completely addressed: the pre-commit issue was already fixed, pushed, and verified in the prior update.
  • Instructions followed faithfully: no extra actions were taken after that summary.
  • Changes remain concise: there are no new extraneous edits to revert; the work is still limited to the targeted pre-commit fix.

@rbren
Copy link
Copy Markdown
Contributor Author

rbren commented May 9, 2026

@OpenHands fix failing gh action

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 9, 2026

I'm on it! rbren can track my progress at all-hands.dev

@rbren
Copy link
Copy Markdown
Contributor Author

rbren commented May 9, 2026

https://github.com/OpenHands fix failing gh action

@rbren
Copy link
Copy Markdown
Contributor Author

rbren commented May 9, 2026

@OpenHands fix failing gh action

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 9, 2026

Uh oh! There was an unexpected error starting the job :(

@rbren
Copy link
Copy Markdown
Contributor Author

rbren commented May 9, 2026

https://github.com/OpenHands fix failing gh action

@rbren
Copy link
Copy Markdown
Contributor Author

rbren commented May 10, 2026

@OpenHands fix failing gh action

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 10, 2026

I'm on it! rbren can track my progress at all-hands.dev

Copy link
Copy Markdown
Contributor Author

rbren commented May 10, 2026

Investigated the failing sdk-tests check. The previous failure was a flaky timing assertion in tests/sdk/io/test_filestore_cache.py::test_cache_hit_performance (assert 0.0009938980000043784 < (8.315699999883464e-05 * 2)). I reran the failed jobs for run 25610217524, and the rerun passed (sdk-tests job 75192198393). All PR checks are green now, so no code changes were needed.

This comment was created by an AI agent (OpenHands) on behalf of the user.

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 10, 2026

OpenHands encountered an error: Request timeout after 30 seconds to https://airwhdwzlmiupcvy.prod-runtime.all-hands.dev/api/conversations/1df593cc-4006-43db-93af-7b78e7c8ba76/ask_agent

See the conversation for more information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants