| author | |
| committer | |
| log | f145a453b56828af4ad61263ca267ebaeec5a141 |
| tree | c055f6ceef8fab3edae3f4ba9aa1c01b595db2c3 |
| parent | 1f4f693dd6525814996741c32a97dda0ed574939 |
- fetch tracked remote after checkout to update ahead/behind counts
- add integration test for behind count when remote branch advances
- invalidate git React Query data on `turn/completed` events in the web app3 files changed, 75 insertions(+), 2 deletions(-)
apps/server/src/git.test.ts+40| ... | ... | @@ -239,6 +239,46 @@ describe("git integration", () => { |
| 239 | 239 | expect(current!.name).toBe("feature"); |
| 240 | 240 | }); |
| 241 | 241 | |
| 242 | it("refreshes upstream behind count after checkout when remote branch advanced", async () => { | |
| 243 | await using remote = await makeTmpDir(); | |
| 244 | await using source = await makeTmpDir(); | |
| 245 | await using clone = await makeTmpDir(); | |
| 246 | await git(remote.path, "init --bare"); | |
| 247 | ||
| 248 | await initRepoWithCommit(source.path); | |
| 249 | const defaultBranch = (await listGitBranches({ cwd: source.path })).branches.find( | |
| 250 | (branch) => branch.current, | |
| 251 | )!.name; | |
| 252 | await git(source.path, `remote add origin ${JSON.stringify(remote.path)}`); | |
| 253 | await git(source.path, `push -u origin ${defaultBranch}`); | |
| 254 | ||
| 255 | const featureBranch = "feature-behind"; | |
| 256 | await createGitBranch({ cwd: source.path, branch: featureBranch }); | |
| 257 | await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); | |
| 258 | await writeFile(path.join(source.path, "feature.txt"), "feature base\n"); | |
| 259 | await git(source.path, "add feature.txt"); | |
| 260 | await git(source.path, "commit -m 'feature base'"); | |
| 261 | await git(source.path, `push -u origin ${featureBranch}`); | |
| 262 | await checkoutGitBranch({ cwd: source.path, branch: defaultBranch }); | |
| 263 | ||
| 264 | await git(clone.path, `clone ${JSON.stringify(remote.path)} .`); | |
| 265 | await git(clone.path, "config user.email 'test@test.com'"); | |
| 266 | await git(clone.path, "config user.name 'Test'"); | |
| 267 | await git(clone.path, `checkout -b ${featureBranch} --track origin/${featureBranch}`); | |
| 268 | await writeFile(path.join(clone.path, "feature.txt"), "feature from remote\n"); | |
| 269 | await git(clone.path, "add feature.txt"); | |
| 270 | await git(clone.path, "commit -m 'remote feature update'"); | |
| 271 | await git(clone.path, `push origin ${featureBranch}`); | |
| 272 | ||
| 273 | await checkoutGitBranch({ cwd: source.path, branch: featureBranch }); | |
| 274 | const core = new GitCoreService(); | |
| 275 | const details = await core.statusDetails(source.path); | |
| 276 | ||
| 277 | expect(details.branch).toBe(featureBranch); | |
| 278 | expect(details.aheadCount).toBe(0); | |
| 279 | expect(details.behindCount).toBe(1); | |
| 280 | }); | |
| 281 | ||
| 242 | 282 | it("throws when branch does not exist", async () => { |
| 243 | 283 | await using tmp = await makeTmpDir(); |
| 244 | 284 | await initRepoWithCommit(tmp.path); |
apps/server/src/git.ts+28| ... | ... | @@ -553,11 +553,39 @@ export class GitCoreService { |
| 553 | 553 | }); |
| 554 | 554 | } |
| 555 | 555 | |
| 556 | private async refreshCheckedOutBranchUpstream(cwd: string): Promise<void> { | |
| 557 | const upstreamRef = trimStdout( | |
| 558 | await this.gitStdout( | |
| 559 | cwd, | |
| 560 | ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], | |
| 561 | true, | |
| 562 | ), | |
| 563 | ); | |
| 564 | if (upstreamRef.length === 0 || upstreamRef === "@{upstream}") { | |
| 565 | return; | |
| 566 | } | |
| 567 | ||
| 568 | const separatorIndex = upstreamRef.indexOf("/"); | |
| 569 | if (separatorIndex <= 0) { | |
| 570 | return; | |
| 571 | } | |
| 572 | const remoteName = upstreamRef.slice(0, separatorIndex); | |
| 573 | if (remoteName.length === 0) { | |
| 574 | return; | |
| 575 | } | |
| 576 | ||
| 577 | await executeGit(cwd, ["fetch", "--quiet", "--no-tags", remoteName], { | |
| 578 | timeoutMs: 20_000, | |
| 579 | allowNonZeroExit: true, | |
| 580 | }); | |
| 581 | } | |
| 582 | ||
| 556 | 583 | async checkoutBranch(input: GitCheckoutInput): Promise<void> { |
| 557 | 584 | await executeGit(input.cwd, ["checkout", input.branch], { |
| 558 | 585 | timeoutMs: 10_000, |
| 559 | 586 | fallbackErrorMessage: "git checkout failed", |
| 560 | 587 | }); |
| 588 | await this.refreshCheckedOutBranchUpstream(input.cwd); | |
| 561 | 589 | } |
| 562 | 590 | |
| 563 | 591 | async initRepo(input: GitInitInput): Promise<void> { |
apps/web/src/App.tsx+7-2| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | |
| 1 | import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; | |
| 2 | 2 | import { useEffect, useRef } from "react"; |
| 3 | 3 | |
| 4 | 4 | import ChatView from "./components/ChatView"; |
| ... | ... | @@ -11,22 +11,27 @@ import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./ty |
| 11 | 11 | import { onServerWelcome } from "./wsNativeApi"; |
| 12 | 12 | import { useNativeApi } from "./hooks/useNativeApi"; |
| 13 | 13 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; |
| 14 | import { invalidateGitQueries } from "./lib/gitReactQuery"; | |
| 14 | 15 | |
| 15 | 16 | function EventRouter() { |
| 16 | 17 | const api = useNativeApi(); |
| 17 | 18 | const { dispatch } = useStore(); |
| 19 | const queryClient = useQueryClient(); | |
| 18 | 20 | const activeAssistantItemRef = useRef<string | null>(null); |
| 19 | 21 | |
| 20 | 22 | useEffect(() => { |
| 21 | 23 | if (!api) return; |
| 22 | 24 | return api.providers.onEvent((event) => { |
| 25 | if (event.method === "turn/completed") { | |
| 26 | void invalidateGitQueries(queryClient); | |
| 27 | } | |
| 23 | 28 | dispatch({ |
| 24 | 29 | type: "APPLY_EVENT", |
| 25 | 30 | event, |
| 26 | 31 | activeAssistantItemRef, |
| 27 | 32 | }); |
| 28 | 33 | }); |
| 29 | }, [api, dispatch]); | |
| 34 | }, [api, dispatch, queryClient]); | |
| 30 | 35 | |
| 31 | 36 | useEffect(() => { |
| 32 | 37 | if (!api) return; |