diff --git a/__tests__/hooks/query/use-local-git-info.test.tsx b/__tests__/hooks/query/use-local-git-info.test.tsx index 6447278d8..fa8f32463 100644 --- a/__tests__/hooks/query/use-local-git-info.test.tsx +++ b/__tests__/hooks/query/use-local-git-info.test.tsx @@ -117,17 +117,12 @@ describe("useLocalGitInfo", () => { it("probes git metadata via the bash runner on a local backend when conversation metadata is incomplete", async () => { // Arrange useActiveBackendMock.mockReturnValue(makeBackend("local")); - runCommandMock - .mockResolvedValueOnce({ - exit_code: 0, - stdout: "git@github.com:acme/widgets.git\n", - stderr: "", - }) - .mockResolvedValueOnce({ - exit_code: 0, - stdout: "main\n", - stderr: "", - }); + // The consolidated script returns remote URL and branch on separate lines. + runCommandMock.mockResolvedValueOnce({ + exit_code: 0, + stdout: "git@github.com:acme/widgets.git\nmain", + stderr: "", + }); // Act const { result } = renderHook(() => useLocalGitInfo(), { @@ -141,8 +136,10 @@ describe("useLocalGitInfo", () => { conversationWithoutRepo.session_api_key, true, ); + // All git probing is now done in a single bash round-trip. + expect(runCommandMock).toHaveBeenCalledTimes(1); expect(runCommandMock).toHaveBeenCalledWith( - "git remote get-url origin", + expect.stringContaining("git remote get-url origin"), "/workspace/project", 10, ); diff --git a/src/hooks/query/use-local-git-info.ts b/src/hooks/query/use-local-git-info.ts index d56b90a3c..74390dc35 100644 --- a/src/hooks/query/use-local-git-info.ts +++ b/src/hooks/query/use-local-git-info.ts @@ -30,19 +30,38 @@ type RunCommand = ( timeout: number, ) => Promise; -async function probeGitInfoAtDir( +// Single shell script that replaces the former probeGitInfoAtDir + +// probeNestedRepoInDir pair. It runs as one bash WebSocket round-trip: +// 1. Read the origin remote URL and current branch at the workspace root. +// 2. If neither is set, search for exactly one nested git repo up to 4 +// levels deep and repeat the probe there. +// Output: two lines — \n — either may be empty. +const GIT_INFO_COMMAND = [ + "r=$(git remote get-url origin 2>/dev/null)", + "b=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)", + 'if [ -z "$r$b" ]; then', + "n=$(find . -mindepth 2 -maxdepth 4 -name .git 2>/dev/null | cut -c3- | sed 's|/.git$||' | sort -u)", + "c=$(printf '%s\\n' \"$n\" | grep -c '[^[:space:]]')", + 'if [ "$c" = "1" ] && [ -n "$n" ]; then', + 'r=$(git -C "$n" remote get-url origin 2>/dev/null)', + 'b=$(git -C "$n" rev-parse --abbrev-ref HEAD 2>/dev/null)', + "fi", + "fi", + 'printf \'%s\\n%s\' "$r" "$b"', +].join("\n"); + +async function probeGitInfo( run: RunCommand, directory: string, ): Promise { - const [remoteResult, branchResult] = await Promise.all([ - run("git remote get-url origin", directory, 10), - run("git rev-parse --abbrev-ref HEAD", directory, 10), - ]); - - const remoteUrl = - remoteResult.exit_code === 0 ? remoteResult.stdout.trim() : ""; - const rawBranch = - branchResult.exit_code === 0 ? branchResult.stdout.trim() : ""; + const result = await run(GIT_INFO_COMMAND, directory, 10); + if (result.exit_code !== 0) return EMPTY_LOCAL_GIT_INFO; + + const nl = result.stdout.indexOf("\n"); + const remoteUrl = ( + nl >= 0 ? result.stdout.slice(0, nl) : result.stdout + ).trim(); + const rawBranch = (nl >= 0 ? result.stdout.slice(nl + 1) : "").trim(); const branch = rawBranch && rawBranch !== "HEAD" ? rawBranch : null; if (!remoteUrl && !branch) return EMPTY_LOCAL_GIT_INFO; @@ -56,37 +75,10 @@ async function probeGitInfoAtDir( }; } -async function probeNestedRepoInDir( - run: RunCommand, - directory: string, -): Promise { - const nestedReposResult = await run( - "find . -mindepth 2 -maxdepth 4 -name .git 2>/dev/null | sed 's#^\\./##' | sed 's#/.git$##'", - directory, - 10, - ); - - if (nestedReposResult.exit_code !== 0) return EMPTY_LOCAL_GIT_INFO; - - const nestedRepos = Array.from( - new Set( - nestedReposResult.stdout - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean), - ), - ); - - if (nestedRepos.length !== 1) return EMPTY_LOCAL_GIT_INFO; - - const nestedDir = `${directory}/${nestedRepos[0]}`.replace(/\/+/g, "/"); - return probeGitInfoAtDir(run, nestedDir); -} - /** * Probe git metadata for a **local** backend's workspace checkout by - * shelling out via the agent server (`git remote get-url origin`, - * `git rev-parse --abbrev-ref HEAD`). + * shelling out via the agent server using a single consolidated bash + * script (see `GIT_INFO_COMMAND`). * * Local-only by design. On cloud backends the conversation metadata * (`selected_repository`, `git_provider`, `selected_branch`) is the @@ -154,15 +146,7 @@ export const useLocalGitInfo = () => { queryFn: async () => { const run: RunCommand = (command, cwd, timeout) => runCommandRef.current(command, cwd, timeout); - const directInfo = await probeGitInfoAtDir(run, workingDir); - if (directInfo.repository || directInfo.branch) return directInfo; - - // Common local flow: user starts in a non-git parent workspace and - // clones a single repository into a child directory. - const nestedInfo = await probeNestedRepoInDir(run, workingDir); - if (nestedInfo.repository || nestedInfo.branch) return nestedInfo; - - return EMPTY_LOCAL_GIT_INFO; + return probeGitInfo(run, workingDir); }, enabled: queryEnabled, retry: false,