diff --git a/apps/server/src/git.test.ts b/apps/server/src/git.test.ts index d20825bc93c39ef73f4b5b44c74ec103bf4a8a8c..9635998ce07f22d7c8a05e2903b7aa235bddfb88 100644 --- a/apps/server/src/git.test.ts +++ b/apps/server/src/git.test.ts @@ -239,6 +239,46 @@ describe("git integration", () => { expect(current!.name).toBe("feature"); }); + it("refreshes upstream behind count after checkout when remote branch advanced", async () => { + await using remote = await makeTmpDir(); + await using source = await makeTmpDir(); + await using clone = await makeTmpDir(); + await git(remote.path, "init --bare"); + + await initRepoWithCommit(source.path); + const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find( + (branch) => branch.current, + )!.name; + await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`); + await git(source.path, `push -u origin ${defaultBranch}`); + + const featureBranch = "feature-behind"; + await createGitBranch({ cwd: source.path, branch: featureBranch }); + await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); + await writeFile(path.join(source.path, "feature.txt"), "feature base\n"); + await git(source.path, "add feature.txt"); + await git(source.path, "commit -m 'feature base'"); + await git(source.path, `push -u origin ${featureBranch}`); + await checkoutGitBranch({ cwd: source.path, branch: defaultBranch }); + + await git(clone.path, `clone ${JSON.stringify(remote.path)} .`); + await git(clone.path, "config user.email 'test@test.com'"); + await git(clone.path, "config user.name 'Test'"); + await git(clone.path, `checkout -b ${featureBranch} --track origin/${featureBranch}`); + await writeFile(path.join(clone.path, "feature.txt"), "feature from remote\n"); + await git(clone.path, "add feature.txt"); + await git(clone.path, "commit -m 'remote feature update'"); + await git(clone.path, `push origin ${featureBranch}`); + + await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); + const core = new GitCoreService(); + const details = await core.statusDetails(source.path); + + expect(details.branch).toBe(featureBranch); + expect(details.aheadCount).toBe(0); + expect(details.behindCount).toBe(1); + }); + it("throws when branch does not exist", async () => { await using tmp = await makeTmpDir(); await initRepoWithCommit(tmp.path); diff --git a/apps/server/src/git.ts b/apps/server/src/git.ts index 0065e15b3beae2b878720fa4cfff01ec279fa6a1..31f85bf02178c9e4a314e3d00b1c8405ff4ce926 100644 --- a/apps/server/src/git.ts +++ b/apps/server/src/git.ts @@ -553,11 +553,39 @@ export class GitCoreService { }); } + private async refreshCheckedOutBranchUpstream(cwd: string): Promise { + const upstreamRef = trimStdout( + await this.gitStdout( + cwd, + ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], + true, + ), + ); + if (upstreamRef.length === 0 || upstreamRef === "@{upstream}") { + return; + } + + const separatorIndex = upstreamRef.indexOf("/"); + if (separatorIndex <= 0) { + return; + } + const remoteName = upstreamRef.slice(0, separatorIndex); + if (remoteName.length === 0) { + return; + } + + await executeGit(cwd, ["fetch", "--quiet", "--no-tags", remoteName], { + timeoutMs: 20_000, + allowNonZeroExit: true, + }); + } + async checkoutBranch(input: GitCheckoutInput): Promise { await executeGit(input.cwd, ["checkout", input.branch], { timeoutMs: 10_000, fallbackErrorMessage: "git checkout failed", }); + await this.refreshCheckedOutBranchUpstream(input.cwd); } async initRepo(input: GitInitInput): Promise { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 8a7e6c0fff8e971d62734db38a888dd55bcaee5b..fbb1177bf1bbfe6f2170a338e374082dbfc48cbd 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,4 +1,4 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; import ChatView from "./components/ChatView"; @@ -11,22 +11,27 @@ import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./ty import { onServerWelcome } from "./wsNativeApi"; import { useNativeApi } from "./hooks/useNativeApi"; import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; +import { invalidateGitQueries } from "./lib/gitReactQuery"; function EventRouter() { const api = useNativeApi(); const { dispatch } = useStore(); + const queryClient = useQueryClient(); const activeAssistantItemRef = useRef(null); useEffect(() => { if (!api) return; return api.providers.onEvent((event) => { + if (event.method === "turn/completed") { + void invalidateGitQueries(queryClient); + } dispatch({ type: "APPLY_EVENT", event, activeAssistantItemRef, }); }); - }, [api, dispatch]); + }, [api, dispatch, queryClient]); useEffect(() => { if (!api) return;