diff --git a/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts b/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts index 13f27f2e853..4d23f60f135 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts @@ -1,7 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; -import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; +import { workspaceTrpc } from "@superset/workspace-client"; +import { useCallback, useMemo } from "react"; import { useWorkspaceEvent } from "../useWorkspaceEvent"; -import { useWorkspaceHostUrl } from "../useWorkspaceHostUrl"; export interface DiffStats { additions: number; @@ -9,54 +8,46 @@ export interface DiffStats { } /** - * Fetches diff stats for a single workspace, auto-updates on git changes. - * Just pass the workspaceId — host resolution is handled internally. + * Diff stats for a single workspace, derived from the shared `git.getStatus` + * query cache. Subscribes to `git:changed` and invalidates the query — React + * Query collapses concurrent invalidations from sibling consumers (e.g. + * `useGitStatus`, multiple sidebar tiles) into a single refetch. */ export function useDiffStats(workspaceId: string): DiffStats | null { - const [stats, setStats] = useState(null); - const hostUrl = useWorkspaceHostUrl(workspaceId); - - const fetchStats = useCallback(async () => { - if (!hostUrl) return; - try { - const client = getHostServiceClientByUrl(hostUrl); - const status = await client.git.getStatus.query({ workspaceId }); - - // Deduplicate by path — a file can appear in multiple categories - const byPath = new Map< - string, - { additions: number; deletions: number } - >(); - for (const file of status.againstBase) { - byPath.set(file.path, file); - } - for (const file of status.staged) { - byPath.set(file.path, file); - } - for (const file of status.unstaged) { - byPath.set(file.path, file); - } - - let additions = 0; - let deletions = 0; - for (const file of byPath.values()) { - additions += file.additions; - deletions += file.deletions; - } - - setStats({ additions, deletions }); - } catch { - // Host unavailable or workspace deleted + const utils = workspaceTrpc.useUtils(); + const { data: status } = workspaceTrpc.git.getStatus.useQuery( + { workspaceId }, + { + enabled: Boolean(workspaceId), + // Match the pre-RQ behavior: only update on `git:changed`, never + // on focus. Multiple sidebar tiles each have their own query key, + // so focus refetch would re-fan out the very work this hook is + // supposed to consolidate. + refetchOnWindowFocus: false, + }, + ); + + const invalidate = useCallback(() => { + void utils.git.getStatus.invalidate({ workspaceId }); + }, [utils, workspaceId]); + + useWorkspaceEvent("git:changed", workspaceId, invalidate); + + return useMemo(() => { + if (!status) return null; + + // Deduplicate by path — a file can appear in multiple categories. + const byPath = new Map(); + for (const file of status.againstBase) byPath.set(file.path, file); + for (const file of status.staged) byPath.set(file.path, file); + for (const file of status.unstaged) byPath.set(file.path, file); + + let additions = 0; + let deletions = 0; + for (const file of byPath.values()) { + additions += file.additions; + deletions += file.deletions; } - }, [hostUrl, workspaceId]); - - useEffect(() => { - void fetchStats(); - }, [fetchStats]); - - useWorkspaceEvent("git:changed", workspaceId, () => { - void fetchStats(); - }); - - return stats; + return { additions, deletions }; + }, [status]); } diff --git a/packages/host-service/src/trpc/router/git/git.ts b/packages/host-service/src/trpc/router/git/git.ts index 177f4bc245e..81eaef668c8 100644 --- a/packages/host-service/src/trpc/router/git/git.ts +++ b/packages/host-service/src/trpc/router/git/git.ts @@ -65,27 +65,32 @@ export const gitRouter = router({ const worktreePath = resolveWorktreePath(ctx, input.workspaceId); const git = await ctx.git(worktreePath); - const currentBranchName = ( - await git.revparse(["--abbrev-ref", "HEAD"]).catch(() => "") - ).trim(); - const base = await resolveBaseComparison(git); - - let branchNames: string[] = []; + // `%(HEAD)` emits "*" for the checked-out branch, " " otherwise. + // Single spawn — independent of branch count. Only `name`/`isHead` + // are read by the v2 sidebar's BaseBranchSelector; the other + // per-branch fields the previous implementation computed (upstream, + // ahead/behind, last-commit) cost 4 spawns each and were unused. + let branches: { name: string; isHead: boolean }[] = []; try { const raw = await git.raw([ - "branch", - "--list", - "--format=%(refname:short)", + "for-each-ref", + "refs/heads/", + "--format=%(HEAD)\t%(refname:short)", ]); - branchNames = raw.trim().split("\n").filter(Boolean); + branches = raw + .trim() + .split("\n") + .filter(Boolean) + .map((line) => { + const tab = line.indexOf("\t"); + if (tab < 0) return { name: line, isHead: false }; + return { + isHead: line.slice(0, tab) === "*", + name: line.slice(tab + 1), + }; + }); } catch {} - const branches = await Promise.all( - branchNames.map((name) => - buildBranch(git, name, name === currentBranchName, base?.baseRef), - ), - ); - return { branches }; }),