From 453f134b3bc213f05db52b22c6a0eb18ef115339 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 28 Jun 2026 14:37:36 -0500 Subject: [PATCH] refactor(desktop): centralize remote git REST routing Keep the remote git mirror as a thin facade: route all GETs through gitGet, all mutations through gitPost, and keep consumers on desktopGit(). On the backend, route git paths through a single _git_path helper instead of repeating str(_fs_path(...)) in every endpoint. Behavior unchanged. --- apps/desktop/src/lib/desktop-git.ts | 65 ++++++++++++++++------------- hermes_cli/web_server.py | 40 ++++++++++-------- 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/apps/desktop/src/lib/desktop-git.ts b/apps/desktop/src/lib/desktop-git.ts index 7d5cda7544e..67a596f04d7 100644 --- a/apps/desktop/src/lib/desktop-git.ts +++ b/apps/desktop/src/lib/desktop-git.ts @@ -16,8 +16,6 @@ import { isDesktopFsRemoteMode } from './desktop-fs' type GitBridge = NonNullable['git']> -const q = (value: string) => encodeURIComponent(value) - function desktopApi(path: string, body?: Record): Promise { const desktop = window.hermesDesktop @@ -28,60 +26,67 @@ function desktopApi(path: string, body?: Record): Promise return desktop.api(body ? { body, method: 'POST', path } : { path }) } +function gitGet(route: string, params: Record): Promise { + const query = new URLSearchParams() + + for (const [key, value] of Object.entries(params)) { + if (value !== null && value !== undefined) { + query.set(key, String(value)) + } + } + + return desktopApi(`/api/git/${route}?${query.toString()}`) +} + +function gitPost(route: string, body: Record): Promise { + return desktopApi(`/api/git/${route}`, body) +} + const remoteGit: GitBridge = { worktreeList: async repoPath => - (await desktopApi<{ worktrees: HermesGitWorktree[] }>(`/api/git/worktrees?path=${q(repoPath)}`)).worktrees, + (await gitGet<{ worktrees: HermesGitWorktree[] }>('worktrees', { path: repoPath })).worktrees, - worktreeAdd: (repoPath, options) => desktopApi(`/api/git/worktree/add`, { path: repoPath, ...options }), + worktreeAdd: (repoPath, options) => gitPost('worktree/add', { path: repoPath, ...options }), worktreeRemove: (repoPath, worktreePath, options) => - desktopApi(`/api/git/worktree/remove`, { force: options?.force ?? false, path: repoPath, worktreePath }), + gitPost('worktree/remove', { force: options?.force ?? false, path: repoPath, worktreePath }), - branchSwitch: (repoPath, branch) => desktopApi(`/api/git/branch/switch`, { branch, path: repoPath }), + branchSwitch: (repoPath, branch) => gitPost('branch/switch', { branch, path: repoPath }), branchList: async repoPath => - (await desktopApi<{ branches: HermesGitBranch[] }>(`/api/git/branches?path=${q(repoPath)}`)).branches, + (await gitGet<{ branches: HermesGitBranch[] }>('branches', { path: repoPath })).branches, - repoStatus: repoPath => desktopApi(`/api/git/status?path=${q(repoPath)}`), + repoStatus: repoPath => gitGet('status', { path: repoPath }), fileDiff: async (repoPath, filePath) => - (await desktopApi<{ diff: string }>(`/api/git/file-diff?path=${q(repoPath)}&file=${q(filePath)}`)).diff, + (await gitGet<{ diff: string }>('file-diff', { file: filePath, path: repoPath })).diff, review: { list: (repoPath, scope, baseRef) => - desktopApi( - `/api/git/review/list?path=${q(repoPath)}&scope=${q(scope)}${baseRef ? `&base=${q(baseRef)}` : ''}` - ), + gitGet('review/list', { base: baseRef, path: repoPath, scope }), diff: async (repoPath, filePath, scope, baseRef, staged) => - ( - await desktopApi<{ diff: string }>( - `/api/git/review/diff?path=${q(repoPath)}&file=${q(filePath)}&scope=${q(scope)}&staged=${staged ? 'true' : 'false'}${baseRef ? `&base=${q(baseRef)}` : ''}` - ) - ).diff, + (await gitGet<{ diff: string }>('review/diff', { base: baseRef, file: filePath, path: repoPath, scope, staged })) + .diff, - stage: (repoPath, filePath) => desktopApi(`/api/git/review/stage`, { file: filePath ?? null, path: repoPath }), + stage: (repoPath, filePath) => gitPost('review/stage', { file: filePath ?? null, path: repoPath }), - unstage: (repoPath, filePath) => desktopApi(`/api/git/review/unstage`, { file: filePath ?? null, path: repoPath }), + unstage: (repoPath, filePath) => gitPost('review/unstage', { file: filePath ?? null, path: repoPath }), - revert: (repoPath, filePath) => desktopApi(`/api/git/review/revert`, { file: filePath ?? null, path: repoPath }), + revert: (repoPath, filePath) => gitPost('review/revert', { file: filePath ?? null, path: repoPath }), revParse: async (repoPath, ref) => - ( - await desktopApi<{ sha: null | string }>( - `/api/git/review/rev-parse?path=${q(repoPath)}${ref ? `&ref=${q(ref)}` : ''}` - ) - ).sha, + (await gitGet<{ sha: null | string }>('review/rev-parse', { path: repoPath, ref })).sha, - commit: (repoPath, message, push) => desktopApi(`/api/git/review/commit`, { message, path: repoPath, push }), + commit: (repoPath, message, push) => gitPost('review/commit', { message, path: repoPath, push }), - commitContext: repoPath => desktopApi(`/api/git/review/commit-context?path=${q(repoPath)}`), + commitContext: repoPath => gitGet('review/commit-context', { path: repoPath }), - push: repoPath => desktopApi(`/api/git/review/push`, { path: repoPath }), + push: repoPath => gitPost('review/push', { path: repoPath }), - shipInfo: repoPath => desktopApi(`/api/git/review/ship-info?path=${q(repoPath)}`), + shipInfo: repoPath => gitGet('review/ship-info', { path: repoPath }), - createPr: repoPath => desktopApi(`/api/git/review/create-pr`, { path: repoPath }) + createPr: repoPath => gitPost('review/create-pr', { path: repoPath }) }, // Repo discovery is a local-disk crawl; on a remote gateway the backend diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index aea336af31e..e0c8c760a4b 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -1931,6 +1931,10 @@ async def _git_op(fn, *args): raise HTTPException(status_code=400, detail=str(exc) or "git operation failed") +def _git_path(path: str) -> str: + return str(_fs_path(path)) + + class GitPathBody(BaseModel): path: str @@ -1967,79 +1971,79 @@ class GitBranchSwitchBody(BaseModel): @app.get("/api/git/status") async def git_status_route(path: str): - return await _git_op(_web_git.repo_status, str(_fs_path(path))) + return await _git_op(_web_git.repo_status, _git_path(path)) @app.get("/api/git/worktrees") async def git_worktrees_route(path: str): - return {"worktrees": await _git_op(_web_git.worktree_list, str(_fs_path(path)))} + return {"worktrees": await _git_op(_web_git.worktree_list, _git_path(path))} @app.get("/api/git/branches") async def git_branches_route(path: str): - return {"branches": await _git_op(_web_git.branch_list, str(_fs_path(path)))} + return {"branches": await _git_op(_web_git.branch_list, _git_path(path))} @app.get("/api/git/review/list") async def git_review_list_route(path: str, scope: str = "uncommitted", base: Optional[str] = None): - return await _git_op(_web_git.review_list, str(_fs_path(path)), scope, base) + return await _git_op(_web_git.review_list, _git_path(path), scope, base) @app.get("/api/git/review/diff") async def git_review_diff_route( path: str, file: str, scope: str = "uncommitted", base: Optional[str] = None, staged: bool = False ): - return {"diff": await _git_op(_web_git.review_diff, str(_fs_path(path)), file, scope, base, staged)} + return {"diff": await _git_op(_web_git.review_diff, _git_path(path), file, scope, base, staged)} @app.get("/api/git/file-diff") async def git_file_diff_route(path: str, file: str): - return {"diff": await _git_op(_web_git.file_diff_vs_head, str(_fs_path(path)), file)} + return {"diff": await _git_op(_web_git.file_diff_vs_head, _git_path(path), file)} @app.get("/api/git/review/commit-context") async def git_commit_context_route(path: str): - return await _git_op(_web_git.review_commit_context, str(_fs_path(path))) + return await _git_op(_web_git.review_commit_context, _git_path(path)) @app.get("/api/git/review/rev-parse") async def git_rev_parse_route(path: str, ref: Optional[str] = None): - return {"sha": await _git_op(_web_git.review_rev_parse, str(_fs_path(path)), ref)} + return {"sha": await _git_op(_web_git.review_rev_parse, _git_path(path), ref)} @app.get("/api/git/review/ship-info") async def git_ship_info_route(path: str): - return await _git_op(_web_git.review_ship_info, str(_fs_path(path))) + return await _git_op(_web_git.review_ship_info, _git_path(path)) @app.post("/api/git/review/stage") async def git_stage_route(body: GitFileBody): - return await _git_op(_web_git.review_stage, str(_fs_path(body.path)), body.file) + return await _git_op(_web_git.review_stage, _git_path(body.path), body.file) @app.post("/api/git/review/unstage") async def git_unstage_route(body: GitFileBody): - return await _git_op(_web_git.review_unstage, str(_fs_path(body.path)), body.file) + return await _git_op(_web_git.review_unstage, _git_path(body.path), body.file) @app.post("/api/git/review/revert") async def git_revert_route(body: GitFileBody): - return await _git_op(_web_git.review_revert, str(_fs_path(body.path)), body.file) + return await _git_op(_web_git.review_revert, _git_path(body.path), body.file) @app.post("/api/git/review/commit") async def git_commit_route(body: GitCommitBody): - return await _git_op(_web_git.review_commit, str(_fs_path(body.path)), body.message, body.push) + return await _git_op(_web_git.review_commit, _git_path(body.path), body.message, body.push) @app.post("/api/git/review/push") async def git_push_route(body: GitPathBody): - return await _git_op(_web_git.review_push, str(_fs_path(body.path))) + return await _git_op(_web_git.review_push, _git_path(body.path)) @app.post("/api/git/review/create-pr") async def git_create_pr_route(body: GitPathBody): - return await _git_op(_web_git.review_create_pr, str(_fs_path(body.path))) + return await _git_op(_web_git.review_create_pr, _git_path(body.path)) @app.post("/api/git/worktree/add") @@ -2054,19 +2058,19 @@ async def git_worktree_add_route(body: GitWorktreeAddBody): }.items() if value } - return await _git_op(_web_git.worktree_add, str(_fs_path(body.path)), options) + return await _git_op(_web_git.worktree_add, _git_path(body.path), options) @app.post("/api/git/worktree/remove") async def git_worktree_remove_route(body: GitWorktreeRemoveBody): return await _git_op( - _web_git.worktree_remove, str(_fs_path(body.path)), str(_fs_path(body.worktreePath)), body.force + _web_git.worktree_remove, _git_path(body.path), _git_path(body.worktreePath), body.force ) @app.post("/api/git/branch/switch") async def git_branch_switch_route(body: GitBranchSwitchBody): - return await _git_op(_web_git.branch_switch, str(_fs_path(body.path)), body.branch) + return await _git_op(_web_git.branch_switch, _git_path(body.path), body.branch) @app.get("/api/status")