Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions __tests__/hooks/query/use-local-git-info.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {
Expand All @@ -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,
);
Expand Down
80 changes: 32 additions & 48 deletions src/hooks/query/use-local-git-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,38 @@ type RunCommand = (
timeout: number,
) => Promise<CommandResult>;

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 — <remote-url>\n<branch> — 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<LocalGitInfo> {
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;
Expand All @@ -56,37 +75,10 @@ async function probeGitInfoAtDir(
};
}

async function probeNestedRepoInDir(
run: RunCommand,
directory: string,
): Promise<LocalGitInfo> {
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
Expand Down Expand Up @@ -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,
Expand Down
Loading