openhands-agent-server 1.34.0__tar.gz → 1.35.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/docker/Dockerfile +1 -1
  3. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/file_router.py +23 -5
  4. openhands_agent_server-1.35.0/openhands/agent_server/git_router.py +181 -0
  5. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/llm_router.py +3 -2
  6. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  7. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/pyproject.toml +1 -1
  8. openhands_agent_server-1.34.0/openhands/agent_server/git_router.py +0 -88
  9. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/__init__.py +0 -0
  10. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/__main__.py +0 -0
  11. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  12. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/agent_profiles_router.py +0 -0
  13. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/api.py +0 -0
  14. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/auth_router.py +0 -0
  15. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/bash_router.py +0 -0
  16. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/bash_service.py +0 -0
  17. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/config.py +0 -0
  18. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/conversation_lease.py +0 -0
  19. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/conversation_router.py +0 -0
  20. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/conversation_service.py +0 -0
  21. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/dependencies.py +0 -0
  22. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/desktop_router.py +0 -0
  23. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/desktop_service.py +0 -0
  24. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/docker/build.py +0 -0
  25. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
  26. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/env_parser.py +0 -0
  27. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/event_compat.py +0 -0
  28. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/event_router.py +0 -0
  29. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/event_service.py +0 -0
  30. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/hooks_router.py +0 -0
  31. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/hooks_service.py +0 -0
  32. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/init_router.py +0 -0
  33. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/logging_config.py +0 -0
  34. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
  35. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/mcp_router.py +0 -0
  36. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/middleware.py +0 -0
  37. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/models.py +0 -0
  38. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/openai/__init__.py +0 -0
  39. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/openai/models.py +0 -0
  40. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/openai/router.py +0 -0
  41. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/openai/service.py +0 -0
  42. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/openapi.py +0 -0
  43. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/persistence/__init__.py +0 -0
  44. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/persistence/models.py +0 -0
  45. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/persistence/store.py +0 -0
  46. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/plugins_router.py +0 -0
  47. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/plugins_service.py +0 -0
  48. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/profiles_router.py +0 -0
  49. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/pub_sub.py +0 -0
  50. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/py.typed +0 -0
  51. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/server_details_router.py +0 -0
  52. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/settings_router.py +0 -0
  53. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/skills_router.py +0 -0
  54. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/skills_service.py +0 -0
  55. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/sockets.py +0 -0
  56. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/sub_agents_router.py +0 -0
  57. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/tool_preload_service.py +0 -0
  58. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/tool_router.py +0 -0
  59. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/utils.py +0 -0
  60. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  61. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  62. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/vscode_router.py +0 -0
  63. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/vscode_service.py +0 -0
  64. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/workspace_router.py +0 -0
  65. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands/agent_server/workspaces_router.py +0 -0
  66. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
  67. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  68. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  69. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands_agent_server.egg-info/requires.txt +0 -0
  70. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  71. {openhands_agent_server-1.34.0 → openhands_agent_server-1.35.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.34.0
3
+ Version: 1.35.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -181,7 +181,7 @@ RUN set -ux; \
181
181
  PATH="$ACP_NODE_DIR/bin:$PATH"; \
182
182
  if "$ACP_NODE_DIR/bin/npm" install -g \
183
183
  @agentclientprotocol/claude-agent-acp@0.44.0 \
184
- @zed-industries/codex-acp@0.16.0 \
184
+ @agentclientprotocol/codex-acp@1.1.2 \
185
185
  @google/gemini-cli@0.46.0; then \
186
186
  # Create wrappers in /usr/local/bin that prepend ACP's Node 22 to PATH.
187
187
  # This ensures the ACP binary's #!/usr/bin/env node shebang resolves
@@ -10,6 +10,7 @@ import tempfile
10
10
  import zipfile
11
11
  from pathlib import Path, PurePosixPath
12
12
  from typing import IO, Annotated, Literal
13
+ from urllib.parse import quote
13
14
  from uuid import UUID
14
15
 
15
16
  from fastapi import (
@@ -30,6 +31,7 @@ from openhands.agent_server.server_details_router import update_last_execution_t
30
31
  from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
31
32
  from openhands.sdk.git.utils import (
32
33
  GIT_EMPTY_TREE_HASH,
34
+ get_git_repository_metadata,
33
35
  get_valid_ref,
34
36
  run_git_command,
35
37
  validate_git_repository,
@@ -323,6 +325,11 @@ def _head_is_detached(root: Path) -> bool:
323
325
  return branch.strip() == "HEAD"
324
326
 
325
327
 
328
+ def _header_safe(value: str) -> str:
329
+ """Percent-encode a header value."""
330
+ return quote(value, safe="")
331
+
332
+
326
333
  def _create_git_delta(
327
334
  root: Path, base_ref: str | None, output_path: Path, excludes: list[str]
328
335
  ) -> str:
@@ -972,13 +979,24 @@ async def archive_directory(
972
979
  detail=f"Failed to archive directory: {str(e)}",
973
980
  )
974
981
 
975
- headers: dict[str, str] | None = None
982
+ # Repo identity (remote / branch / HEAD) applies to both formats so a
983
+ # tar.gz snapshot is self-describing too; base_commit/base_ref are
984
+ # git-delta-only (they describe the patch's replay base).
985
+ headers: dict[str, str] = {}
986
+ if (repo_root / ".git").exists():
987
+ repo_metadata = await asyncio.to_thread(get_git_repository_metadata, repo_root)
988
+ for key, header in (
989
+ ("repo_remote", "X-Archive-Repo-Remote"),
990
+ ("branch", "X-Archive-Branch"),
991
+ ("head_commit", "X-Archive-Head-Commit"),
992
+ ):
993
+ if value := repo_metadata.get(key):
994
+ headers[header] = _header_safe(value)
995
+ headers["X-Archive-Repo-Root"] = _header_safe(str(repo_root))
976
996
  if base_commit:
977
997
  # Make a git-delta self-describing so consumers can replay the patch.
978
- headers = {
979
- "X-Archive-Base-Commit": base_commit,
980
- "X-Archive-Base-Ref": base_ref or "auto",
981
- }
998
+ headers["X-Archive-Base-Commit"] = base_commit
999
+ headers["X-Archive-Base-Ref"] = base_ref or "auto"
982
1000
 
983
1001
  return FileResponse(
984
1002
  path=output_path,
@@ -0,0 +1,181 @@
1
+ """Git router for OpenHands SDK."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ from fastapi import APIRouter, HTTPException, Path as PathParam, Query
9
+
10
+ from openhands.agent_server.server_details_router import update_last_execution_time
11
+ from openhands.sdk.git.exceptions import GitError, GitRepositoryError
12
+ from openhands.sdk.git.git_changes import get_git_changes
13
+ from openhands.sdk.git.git_commits import (
14
+ get_commit_changes,
15
+ get_commit_file_diff,
16
+ get_git_commits,
17
+ )
18
+ from openhands.sdk.git.git_diff import get_git_diff
19
+ from openhands.sdk.git.models import GitChange, GitCommitsPage, GitDiff
20
+
21
+
22
+ git_router = APIRouter(prefix="/git", tags=["Git"])
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ _REF_QUERY_DESCRIPTION = (
27
+ "Optional git ref to diff against (e.g. 'HEAD' for git status-style "
28
+ "changes, or a commit hash). When omitted, the upstream/default branch "
29
+ "is auto-detected."
30
+ )
31
+
32
+ _COMMIT_QUERY_DESCRIPTION = (
33
+ "Optional commit SHA. When set, the diff is the change that commit "
34
+ "introduced (vs its first parent), read entirely from git objects so "
35
+ "deleted files still render. Mutually exclusive with 'ref'."
36
+ )
37
+
38
+ # Hex-only so a path/query value can never reach git argv as an option
39
+ # (list-args protect against shell injection, not option injection).
40
+ _SHA_PATTERN = r"^[0-9a-fA-F]{4,64}$"
41
+
42
+
43
+ async def _get_git_changes(path: str, ref: str | None) -> list[GitChange]:
44
+ """Internal helper to get git changes for a given path."""
45
+ update_last_execution_time()
46
+ loop = asyncio.get_running_loop()
47
+ try:
48
+ return await loop.run_in_executor(
49
+ None, functools.partial(get_git_changes, Path(path), ref=ref)
50
+ )
51
+ except GitRepositoryError:
52
+ # A non-repo workspace has no git changes to report; respond with an
53
+ # empty list so the Changes tab can render normally instead of 500ing.
54
+ logger.debug("Path %s is not a git repository; returning no changes", path)
55
+ return []
56
+
57
+
58
+ async def _get_git_diff(path: str, ref: str | None) -> GitDiff:
59
+ """Internal helper to get git diff for a given path."""
60
+ update_last_execution_time()
61
+ loop = asyncio.get_running_loop()
62
+ try:
63
+ return await loop.run_in_executor(
64
+ None, functools.partial(get_git_diff, Path(path), ref=ref)
65
+ )
66
+ except GitRepositoryError:
67
+ # Only collapse the not-a-repo case to an empty diff; file-level
68
+ # GitPathError (missing/oversize/outside-repo) stays a 500 so
69
+ # callers can distinguish it from "no changes".
70
+ logger.debug("Path %s is not in a git repository; returning empty diff", path)
71
+ return GitDiff(modified=None, original=None)
72
+
73
+
74
+ async def _get_git_commits(path: str, limit: int) -> GitCommitsPage:
75
+ """Internal helper to list commits for a given repo path."""
76
+ update_last_execution_time()
77
+ loop = asyncio.get_running_loop()
78
+ try:
79
+ return await loop.run_in_executor(
80
+ None, functools.partial(get_git_commits, Path(path), limit=limit)
81
+ )
82
+ except GitRepositoryError:
83
+ # A non-repo workspace has no commits to report; respond with an
84
+ # empty page so the commits section can render its empty state.
85
+ logger.debug("Path %s is not a git repository; returning no commits", path)
86
+ return GitCommitsPage(commits=[], has_more=False)
87
+
88
+
89
+ async def _get_commit_changes(path: str, sha: str) -> list[GitChange]:
90
+ """Internal helper to get the files changed by one commit."""
91
+ update_last_execution_time()
92
+ loop = asyncio.get_running_loop()
93
+ try:
94
+ return await loop.run_in_executor(
95
+ None, functools.partial(get_commit_changes, Path(path), sha)
96
+ )
97
+ except GitRepositoryError:
98
+ logger.debug("Path %s is not a git repository; returning no changes", path)
99
+ return []
100
+
101
+
102
+ async def _get_commit_file_diff(path: str, commit: str) -> GitDiff:
103
+ """Internal helper to get one file's diff as changed by one commit."""
104
+ update_last_execution_time()
105
+ loop = asyncio.get_running_loop()
106
+ try:
107
+ return await loop.run_in_executor(
108
+ None, functools.partial(get_commit_file_diff, Path(path), commit)
109
+ )
110
+ except GitRepositoryError:
111
+ logger.debug("Path %s is not in a git repository; returning empty diff", path)
112
+ return GitDiff(modified=None, original=None)
113
+
114
+
115
+ @git_router.get("/changes")
116
+ async def git_changes_query(
117
+ path: str = Query(..., description="The git repository path"),
118
+ ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
119
+ ) -> list[GitChange]:
120
+ """Get git changes using query parameter (preferred method)."""
121
+ try:
122
+ return await _get_git_changes(path, ref)
123
+ except GitError as e:
124
+ # GitRepositoryError is already handled in the helper (returns []).
125
+ # Any remaining GitError subclass (e.g. GitCommandError) surfaces as
126
+ # 400 so the client can show an actionable error instead of an
127
+ # opaque 500.
128
+ raise HTTPException(status_code=400, detail=str(e))
129
+
130
+
131
+ @git_router.get("/diff")
132
+ async def git_diff_query(
133
+ path: str = Query(..., description="The file path to get diff for"),
134
+ ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
135
+ commit: str | None = Query(
136
+ None, pattern=_SHA_PATTERN, description=_COMMIT_QUERY_DESCRIPTION
137
+ ),
138
+ ) -> GitDiff:
139
+ """Get git diff using query parameter (preferred method)."""
140
+ if ref is not None and commit is not None:
141
+ raise HTTPException(
142
+ status_code=400, detail="'ref' and 'commit' are mutually exclusive"
143
+ )
144
+ try:
145
+ if commit is not None:
146
+ return await _get_commit_file_diff(path, commit)
147
+ return await _get_git_diff(path, ref)
148
+ except GitError as e:
149
+ # GitRepositoryError is already handled in the helpers (returns an
150
+ # empty diff). Any remaining GitError subclass (e.g. GitCommandError,
151
+ # GitPathError) surfaces as 400 so the client can show an actionable
152
+ # error instead of an opaque 500.
153
+ raise HTTPException(status_code=400, detail=str(e))
154
+
155
+
156
+ @git_router.get("/commits")
157
+ async def git_commits_query(
158
+ path: str = Query(..., description="The git repository path"),
159
+ limit: int = Query(50, ge=1, le=200, description="Maximum commits to return"),
160
+ ) -> GitCommitsPage:
161
+ """List the repository's most recent commits, newest first."""
162
+ try:
163
+ return await _get_git_commits(path, limit)
164
+ except GitError as e:
165
+ # GitRepositoryError is already handled in the helper (returns an
166
+ # empty page). Any remaining GitError subclass surfaces as 400.
167
+ raise HTTPException(status_code=400, detail=str(e))
168
+
169
+
170
+ @git_router.get("/commits/{sha}/changes")
171
+ async def git_commit_changes_query(
172
+ sha: str = PathParam(..., pattern=_SHA_PATTERN, description="Commit SHA"),
173
+ path: str = Query(..., description="The git repository path"),
174
+ ) -> list[GitChange]:
175
+ """Get the files changed by a single commit (vs its first parent)."""
176
+ try:
177
+ return await _get_commit_changes(path, sha)
178
+ except GitError as e:
179
+ # GitRepositoryError is already handled in the helper (returns []).
180
+ # An unknown/unresolvable sha raises GitCommandError -> 400.
181
+ raise HTTPException(status_code=400, detail=str(e))
@@ -138,9 +138,10 @@ async def list_models(
138
138
  models = sorted(set(all_models))
139
139
  else:
140
140
  filtered_models = []
141
+ verified_provider_models = set(VERIFIED_MODELS.get(provider, ()))
141
142
  for model in all_models:
142
- model_provider, model_id, separator = _extract_model_and_provider(model)
143
- if model_provider == provider:
143
+ model_provider, _, _ = _extract_model_and_provider(model)
144
+ if model_provider == provider or model in verified_provider_models:
144
145
  filtered_models.append(model)
145
146
  models = sorted(set(filtered_models))
146
147
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.34.0
3
+ Version: 1.35.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-agent-server"
3
- version = "1.34.0"
3
+ version = "1.35.0"
4
4
  description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -1,88 +0,0 @@
1
- """Git router for OpenHands SDK."""
2
-
3
- import asyncio
4
- import functools
5
- import logging
6
- from pathlib import Path
7
-
8
- from fastapi import APIRouter, HTTPException, Query
9
-
10
- from openhands.agent_server.server_details_router import update_last_execution_time
11
- from openhands.sdk.git.exceptions import GitError, GitRepositoryError
12
- from openhands.sdk.git.git_changes import get_git_changes
13
- from openhands.sdk.git.git_diff import get_git_diff
14
- from openhands.sdk.git.models import GitChange, GitDiff
15
-
16
-
17
- git_router = APIRouter(prefix="/git", tags=["Git"])
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- _REF_QUERY_DESCRIPTION = (
22
- "Optional git ref to diff against (e.g. 'HEAD' for git status-style "
23
- "changes, or a commit hash). When omitted, the upstream/default branch "
24
- "is auto-detected."
25
- )
26
-
27
-
28
- async def _get_git_changes(path: str, ref: str | None) -> list[GitChange]:
29
- """Internal helper to get git changes for a given path."""
30
- update_last_execution_time()
31
- loop = asyncio.get_running_loop()
32
- try:
33
- return await loop.run_in_executor(
34
- None, functools.partial(get_git_changes, Path(path), ref=ref)
35
- )
36
- except GitRepositoryError:
37
- # A non-repo workspace has no git changes to report; respond with an
38
- # empty list so the Changes tab can render normally instead of 500ing.
39
- logger.debug("Path %s is not a git repository; returning no changes", path)
40
- return []
41
-
42
-
43
- async def _get_git_diff(path: str, ref: str | None) -> GitDiff:
44
- """Internal helper to get git diff for a given path."""
45
- update_last_execution_time()
46
- loop = asyncio.get_running_loop()
47
- try:
48
- return await loop.run_in_executor(
49
- None, functools.partial(get_git_diff, Path(path), ref=ref)
50
- )
51
- except GitRepositoryError:
52
- # Only collapse the not-a-repo case to an empty diff; file-level
53
- # GitPathError (missing/oversize/outside-repo) stays a 500 so
54
- # callers can distinguish it from "no changes".
55
- logger.debug("Path %s is not in a git repository; returning empty diff", path)
56
- return GitDiff(modified=None, original=None)
57
-
58
-
59
- @git_router.get("/changes")
60
- async def git_changes_query(
61
- path: str = Query(..., description="The git repository path"),
62
- ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
63
- ) -> list[GitChange]:
64
- """Get git changes using query parameter (preferred method)."""
65
- try:
66
- return await _get_git_changes(path, ref)
67
- except GitError as e:
68
- # GitRepositoryError is already handled in the helper (returns []).
69
- # Any remaining GitError subclass (e.g. GitCommandError) surfaces as
70
- # 400 so the client can show an actionable error instead of an
71
- # opaque 500.
72
- raise HTTPException(status_code=400, detail=str(e))
73
-
74
-
75
- @git_router.get("/diff")
76
- async def git_diff_query(
77
- path: str = Query(..., description="The file path to get diff for"),
78
- ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
79
- ) -> GitDiff:
80
- """Get git diff using query parameter (preferred method)."""
81
- try:
82
- return await _get_git_diff(path, ref)
83
- except GitError as e:
84
- # GitRepositoryError is already handled in the helper (returns an
85
- # empty diff). Any remaining GitError subclass (e.g. GitCommandError,
86
- # GitPathError) surfaces as 400 so the client can show an actionable
87
- # error instead of an opaque 500.
88
- raise HTTPException(status_code=400, detail=str(e))