Skip to content

Add optional ref query param to /api/git/{changes,diff}#3116

Merged
rbren merged 1 commit intomainfrom
feat/git-ref-param
May 8, 2026
Merged

Add optional ref query param to /api/git/{changes,diff}#3116
rbren merged 1 commit intomainfrom
feat/git-ref-param

Conversation

@rbren
Copy link
Copy Markdown
Contributor

@rbren rbren commented May 8, 2026

Both endpoints currently auto-detect the comparison ref via get_valid_ref() (origin/<current_branch> -> origin/<default_branch> -> merge-base -> empty tree), which is the correct behaviour for diffing a feature branch against the remote. There is no way to ask for 'git status'-style diffs against the local HEAD, which a UI like agent-canvas's Changes tab needs.

This adds an optional 'ref' query parameter to both endpoints:

GET /api/git/changes?path=&ref=HEAD
GET /api/git/diff?path=&ref=HEAD

When provided it is resolved via 'git rev-parse --verify ^{commit}' and used directly as the comparison base, so 'ref=HEAD' yields working tree + index vs the latest commit (i.e. exactly what 'git status' / 'git diff HEAD' would show). Invalid refs surface a GitCommandError so callers can distinguish 'no changes' from 'ref not found'. When omitted, the existing auto-detection behaviour is preserved verbatim, so this is a purely additive, backwards-compatible change.

The ref is plumbed through get_valid_ref(override=...), get_changes_in_repo(ref=...), get_git_changes(ref=...) and get_git_diff(ref=...). New SDK-level tests exercise real temporary repos with multiple commits to confirm ref='HEAD' yields the git-status semantics (and that an invalid ref raises). Router tests verify the parameter is forwarded and that the OpenAPI schema advertises it as an optional query param.

  • 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:0c65267-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:0c65267-golang-amd64
ghcr.io/openhands/agent-server:0c65267-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:0c65267-golang-arm64
ghcr.io/openhands/agent-server:0c65267-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:0c65267-java-amd64
ghcr.io/openhands/agent-server:0c65267-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:0c65267-java-arm64
ghcr.io/openhands/agent-server:0c65267-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:0c65267-python-amd64
ghcr.io/openhands/agent-server:0c65267-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:0c65267-python-arm64
ghcr.io/openhands/agent-server:0c65267-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:0c65267-golang
ghcr.io/openhands/agent-server:0c65267-java
ghcr.io/openhands/agent-server:0c65267-python

About Multi-Architecture Support

  • Each variant tag (e.g., 0c65267-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., 0c65267-python-amd64) are also available if needed

Both endpoints currently auto-detect the comparison ref via
get_valid_ref() (origin/<current_branch> -> origin/<default_branch> ->
merge-base -> empty tree), which is the correct behaviour for diffing
a feature branch against the remote. There is no way to ask for
'git status'-style diffs against the local HEAD, which a UI like
agent-canvas's Changes tab needs.

This adds an optional 'ref' query parameter to both endpoints:

  GET /api/git/changes?path=<repo>&ref=HEAD
  GET /api/git/diff?path=<file>&ref=HEAD

When provided it is resolved via 'git rev-parse --verify <ref>^{commit}'
and used directly as the comparison base, so 'ref=HEAD' yields working
tree + index vs the latest commit (i.e. exactly what 'git status' /
'git diff HEAD' would show). Invalid refs surface a GitCommandError so
callers can distinguish 'no changes' from 'ref not found'. When omitted,
the existing auto-detection behaviour is preserved verbatim, so this is
a purely additive, backwards-compatible change.

The ref is plumbed through get_valid_ref(override=...),
get_changes_in_repo(ref=...), get_git_changes(ref=...) and
get_git_diff(ref=...). New SDK-level tests exercise real temporary
repos with multiple commits to confirm ref='HEAD' yields the
git-status semantics (and that an invalid ref raises). Router tests
verify the parameter is forwarded and that the OpenAPI schema
advertises it as an optional query param.

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

github-actions Bot commented May 8, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/git
   git_changes.py954255%33, 65–66, 76–78, 82–83, 92–93, 106–108, 114, 120–121, 127–128, 134–135, 141–142, 150, 152, 164–166, 173–174, 188–190, 229–230, 232, 236, 244–245, 247, 254–256
   git_diff.py601083%86, 98–99, 104–105, 120–122, 132–133
   utils.py1182777%72–74, 99–101, 153–154, 161–166, 171–172, 182–187, 197–199, 227, 337
TOTAL25984615376% 

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, backward-compatible addition that solves a real need (git-status-style diffs) without affecting existing behavior. The implementation is straightforward:

  • When ref=None (default), preserves existing auto-detection behavior
  • When ref='HEAD' (or other ref), uses git rev-parse --verify to validate and compare against that ref
  • Invalid refs surface as GitCommandError so callers can distinguish 'ref not found' from 'no changes'

The tests are comprehensive - they use real git repos to verify the exact semantics rather than just mocking calls.

[RISK ASSESSMENT]
⚠️ Risk Assessment: 🟢 LOW

Purely additive change with comprehensive test coverage. No security concerns - git validates the ref parameter via rev-parse --verify. No breaking changes.

VERDICT:
Worth merging

KEY INSIGHT:
Using override parameter in get_valid_ref() elegantly preserves the existing auto-detection fallback chain while enabling explicit ref specification when needed.

@rbren rbren merged commit a968259 into main May 8, 2026
31 checks passed
@rbren rbren deleted the feat/git-ref-param branch May 8, 2026 01:31
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

This PR successfully adds an optional ref query parameter to both git endpoints, enabling git status-style diffs when ref=HEAD is provided while preserving backward compatibility when omitted.

Does this PR achieve its stated goal?

Yes. The PR set out to add an optional ref query parameter to /api/git/changes and /api/git/diff that allows callers to request git status-style diffs against a specific ref (e.g., ref=HEAD) instead of relying on auto-detection. I verified this works correctly by:

  1. Creating a test repository with multiple commits and uncommitted changes
  2. Testing SDK functions (get_changes_in_repo(), get_git_diff()) with and without the ref parameter
  3. Starting the agent server and making real HTTP requests to both endpoints
  4. Verifying that ref=HEAD correctly shows only uncommitted changes (git status behavior)
  5. Confirming that omitting ref preserves the original auto-detection behavior
  6. Validating that invalid refs raise GitCommandError as documented

All tests passed, demonstrating the feature works exactly as described in the PR.

Phase Result
Environment Setup ✅ Dependencies installed, agent server started successfully
CI Status ✅ All critical checks passing (sdk-tests, agent-server-tests, API validation, pre-commit)
Functional Verification ✅ Both SDK functions and API endpoints work correctly with new ref parameter
Functional Verification

Test Setup

Created a test git repository with:

  • 2 committed files: committed.txt (first commit), second.txt (second commit)
  • 1 modified file: committed.txt (modified in working tree)
  • 1 untracked file: untracked.txt
cd /tmp/test-git-ref && git init
echo "baseline content" > committed.txt && git add . && git commit -m "first commit"
echo "second commit content" > second.txt && git add . && git commit -m "second commit"
echo "modified" > committed.txt && echo "untracked content" > untracked.txt

SDK-Level Verification

Test 1: get_changes_in_repo() without ref parameter (baseline behavior)

Ran:

changes = get_changes_in_repo("/tmp/test-git-ref")

Result:

Found 3 changes:
  - ADDED      committed.txt
  - ADDED      second.txt
  - ADDED      untracked.txt

Interpretation: Without the ref parameter, auto-detection compares against the empty tree (since no remote exists), showing all files as added. This is the original behavior and confirms backward compatibility.

Test 2: get_changes_in_repo() WITH ref='HEAD' (new behavior)

Ran:

changes = get_changes_in_repo("/tmp/test-git-ref", ref="HEAD")

Result:

Found 2 changes:
  - UPDATED    committed.txt
  - ADDED      untracked.txt

Interpretation: ✅ With ref='HEAD', only uncommitted changes are shown:

  • committed.txt appears as UPDATED (modified in working tree)
  • untracked.txt appears as ADDED (new file)
  • second.txt is correctly excluded (it's committed at HEAD)

This confirms the feature delivers git status-style behavior.

Test 3: Invalid ref raises GitCommandError

Ran:

changes = get_changes_in_repo("/tmp/test-git-ref", ref="definitely-not-a-real-ref")

Result:

GitCommandError: Git command failed: git --no-pager rev-parse --verify 'definitely-not-a-real-ref^{commit}'

Interpretation: ✅ Invalid refs properly raise GitCommandError, allowing callers to distinguish "ref not found" from "no changes".

Test 4: get_git_diff() with ref='HEAD'

Ran:

os.chdir("/tmp/test-git-ref")
diff = get_git_diff("committed.txt", ref="HEAD")

Result:

Original content: 'baseline content'
Modified content: 'modified'

Interpretation: ✅ Diff correctly shows HEAD's content as original ("baseline content") and working tree content as modified ("modified").

API Endpoint Verification

Started agent server on http://127.0.0.1:8000 and made real HTTP requests.

Test 5: GET /api/git/changes without ref parameter

Ran:

curl "http://127.0.0.1:8000/api/git/changes?path=/tmp/test-git-ref"

Result:

[
  {"status": "ADDED", "path": "committed.txt"},
  {"status": "ADDED", "path": "second.txt"},
  {"status": "ADDED", "path": "untracked.txt"}
]

Interpretation: ✅ Auto-detection behavior preserved (backward compatible).

Test 6: GET /api/git/changes?ref=HEAD

Ran:

curl "http://127.0.0.1:8000/api/git/changes?path=/tmp/test-git-ref&ref=HEAD"

Result:

[
  {"status": "UPDATED", "path": "committed.txt"},
  {"status": "ADDED", "path": "untracked.txt"}
]

Interpretation: ✅ Only uncommitted changes returned. second.txt correctly excluded.

Test 7: Invalid ref returns 500 with error message

Ran:

curl "http://127.0.0.1:8000/api/git/changes?path=/tmp/test-git-ref&ref=invalid-ref-xyz"

Result:

{"detail":"Internal Server Error","exception":"Git command failed: git --no-pager rev-parse --verify 'invalid-ref-xyz^{commit}'"}

Status: 500

Interpretation: ✅ Invalid refs properly surface as 500 errors with clear error messages.

Test 8: GET /api/git/diff?ref=HEAD

Ran:

curl "http://127.0.0.1:8000/api/git/diff?path=/tmp/test-git-ref/committed.txt&ref=HEAD"

Result:

{"original": "baseline content", "modified": "modified"}

Interpretation: ✅ Diff endpoint correctly uses ref=HEAD as comparison base.

Test 9: OpenAPI schema includes ref parameter

Ran:

curl "http://127.0.0.1:8000/openapi.json" | jq '.paths["/api/git/changes"].get.parameters'

Result:

[
  {"name": "path", "in": "query", "required": true, ...},
  {"name": "ref", "in": "query", "required": false, 
   "description": "Optional git ref to diff against (e.g. 'HEAD' for git status-style changes, or a commit hash). When omitted, the upstream/default branch is auto-detected."}
]

Interpretation: ✅ Both /api/git/changes and /api/git/diff properly document the new optional ref parameter in the OpenAPI schema.

Unit Test Verification

All new unit tests pass:

✅ test_get_changes_in_repo_ref_head_shows_only_uncommitted PASSED
✅ test_get_changes_in_repo_invalid_ref_raises PASSED
✅ test_get_git_changes_propagates_ref PASSED
✅ test_get_git_diff_ref_head_compares_against_latest_commit PASSED
✅ test_get_git_diff_invalid_ref_raises PASSED
✅ test_git_changes_forwards_ref_query_param PASSED
✅ test_git_diff_forwards_ref_query_param PASSED
✅ test_git_endpoints_expose_ref_query_param PASSED

Existing tests (without ref parameter) also pass, confirming backward compatibility:

✅ test_git_changes_query_param_success PASSED
✅ test_git_diff_query_param_success PASSED

Issues Found

None.

neubig pushed a commit that referenced this pull request May 8, 2026
Co-authored-by: openhands <openhands@all-hands.dev>
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.

2 participants