authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-01 16:52:28-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-01 23:52:28+00:00
logb92c78f55215daf2a62d4cc6d06547029dc9df8f
treeee7dccaba595ef2aa9f3c542234752c6b7e4253d
parent801dfe5b413905553ef1a8ba7ee8b4d941c3fc24
signature Signed by PGP key B5690EEEBB952194

Scope git query invalidation by cwd (#1670)


3 files changed, 84 insertions(+), 5 deletions(-)

apps/web/src/components/GitActionsControl.tsx+4-3
...@@ -47,6 +47,7 @@ import {...@@ -47,6 +47,7 @@ import {
47 gitPullMutationOptions,47 gitPullMutationOptions,
48 gitRunStackedActionMutationOptions,48 gitRunStackedActionMutationOptions,
49 gitStatusQueryOptions,49 gitStatusQueryOptions,
50 invalidateGitStatusQuery,
50 invalidateGitQueries,51 invalidateGitQueries,
51} from "~/lib/gitReactQuery";52} from "~/lib/gitReactQuery";
52import { newCommandId, randomUUID } from "~/lib/utils";53import { newCommandId, randomUUID } from "~/lib/utils";
...@@ -286,8 +287,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -286,8 +287,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
286287
287 useEffect(() => {288 useEffect(() => {
288 if (!isGitStatusOutOfSync) return;289 if (!isGitStatusOutOfSync) return;
289 void invalidateGitQueries(queryClient);290 void invalidateGitQueries(queryClient, { cwd: gitCwd });
290 }, [isGitStatusOutOfSync, queryClient]);291 }, [gitCwd, isGitStatusOutOfSync, queryClient]);
291292
292 const gitStatusForActions = isGitStatusOutOfSync ? null : gitStatus;293 const gitStatusForActions = isGitStatusOutOfSync ? null : gitStatus;
293294
...@@ -814,7 +815,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -814,7 +815,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
814 <GroupSeparator className="hidden @3xl/header-actions:block" />815 <GroupSeparator className="hidden @3xl/header-actions:block" />
815 <Menu816 <Menu
816 onOpenChange={(open) => {817 onOpenChange={(open) => {
817 if (open) void invalidateGitQueries(queryClient);818 if (open) void invalidateGitStatusQuery(queryClient, gitCwd);
818 }}819 }}
819 >820 >
820 <MenuTrigger821 <MenuTrigger
apps/web/src/lib/gitReactQuery.test.ts+63-1
...@@ -1,10 +1,24 @@...@@ -1,10 +1,24 @@
1import { QueryClient } from "@tanstack/react-query";1import { QueryClient } from "@tanstack/react-query";
2import { describe, expect, it } from "vitest";2import { describe, expect, it, vi } from "vitest";
3
4vi.mock("../nativeApi", () => ({
5 ensureNativeApi: vi.fn(),
6}));
7
8vi.mock("../wsRpcClient", () => ({
9 getWsRpcClient: vi.fn(),
10}));
11
3import {12import {
13 gitBranchesQueryOptions,
4 gitMutationKeys,14 gitMutationKeys,
15 gitQueryKeys,
5 gitPreparePullRequestThreadMutationOptions,16 gitPreparePullRequestThreadMutationOptions,
6 gitPullMutationOptions,17 gitPullMutationOptions,
7 gitRunStackedActionMutationOptions,18 gitRunStackedActionMutationOptions,
19 invalidateGitStatusQuery,
20 gitStatusQueryOptions,
21 invalidateGitQueries,
8} from "./gitReactQuery";22} from "./gitReactQuery";
923
10describe("gitMutationKeys", () => {24describe("gitMutationKeys", () => {
...@@ -49,3 +63,51 @@ describe("git mutation options", () => {...@@ -49,3 +63,51 @@ describe("git mutation options", () => {
49 expect(options.mutationKey).toEqual(gitMutationKeys.preparePullRequestThread("/repo/a"));63 expect(options.mutationKey).toEqual(gitMutationKeys.preparePullRequestThread("/repo/a"));
50 });64 });
51});65});
66
67describe("invalidateGitQueries", () => {
68 it("can invalidate a single cwd without blasting other git query scopes", async () => {
69 const queryClient = new QueryClient();
70
71 queryClient.setQueryData(gitQueryKeys.status("/repo/a"), { ok: "a" });
72 queryClient.setQueryData(gitQueryKeys.branches("/repo/a"), { ok: "a-branches" });
73 queryClient.setQueryData(gitQueryKeys.status("/repo/b"), { ok: "b" });
74 queryClient.setQueryData(gitQueryKeys.branches("/repo/b"), { ok: "b-branches" });
75
76 await invalidateGitQueries(queryClient, { cwd: "/repo/a" });
77
78 expect(
79 queryClient.getQueryState(gitStatusQueryOptions("/repo/a").queryKey)?.isInvalidated,
80 ).toBe(true);
81 expect(
82 queryClient.getQueryState(gitBranchesQueryOptions("/repo/a").queryKey)?.isInvalidated,
83 ).toBe(true);
84 expect(
85 queryClient.getQueryState(gitStatusQueryOptions("/repo/b").queryKey)?.isInvalidated,
86 ).toBe(false);
87 expect(
88 queryClient.getQueryState(gitBranchesQueryOptions("/repo/b").queryKey)?.isInvalidated,
89 ).toBe(false);
90 });
91});
92
93describe("invalidateGitStatusQuery", () => {
94 it("invalidates only status for the selected cwd", async () => {
95 const queryClient = new QueryClient();
96
97 queryClient.setQueryData(gitQueryKeys.status("/repo/a"), { ok: "a" });
98 queryClient.setQueryData(gitQueryKeys.branches("/repo/a"), { ok: "a-branches" });
99 queryClient.setQueryData(gitQueryKeys.status("/repo/b"), { ok: "b" });
100
101 await invalidateGitStatusQuery(queryClient, "/repo/a");
102
103 expect(
104 queryClient.getQueryState(gitStatusQueryOptions("/repo/a").queryKey)?.isInvalidated,
105 ).toBe(true);
106 expect(
107 queryClient.getQueryState(gitBranchesQueryOptions("/repo/a").queryKey)?.isInvalidated,
108 ).toBe(false);
109 expect(
110 queryClient.getQueryState(gitStatusQueryOptions("/repo/b").queryKey)?.isInvalidated,
111 ).toBe(false);
112 });
113});
apps/web/src/lib/gitReactQuery.ts+17-1
...@@ -23,10 +23,26 @@ export const gitMutationKeys = {...@@ -23,10 +23,26 @@ export const gitMutationKeys = {
23 ["git", "mutation", "prepare-pull-request-thread", cwd] as const,23 ["git", "mutation", "prepare-pull-request-thread", cwd] as const,
24};24};
2525
26export function invalidateGitQueries(queryClient: QueryClient) {26export function invalidateGitQueries(queryClient: QueryClient, input?: { cwd?: string | null }) {
27 const cwd = input?.cwd ?? null;
28 if (cwd !== null) {
29 return Promise.all([
30 queryClient.invalidateQueries({ queryKey: gitQueryKeys.status(cwd) }),
31 queryClient.invalidateQueries({ queryKey: gitQueryKeys.branches(cwd) }),
32 ]);
33 }
34
27 return queryClient.invalidateQueries({ queryKey: gitQueryKeys.all });35 return queryClient.invalidateQueries({ queryKey: gitQueryKeys.all });
28}36}
2937
38export function invalidateGitStatusQuery(queryClient: QueryClient, cwd: string | null) {
39 if (cwd === null) {
40 return Promise.resolve();
41 }
42
43 return queryClient.invalidateQueries({ queryKey: gitQueryKeys.status(cwd) });
44}
45
30export function gitStatusQueryOptions(cwd: string | null) {46export function gitStatusQueryOptions(cwd: string | null) {
31 return queryOptions({47 return queryOptions({
32 queryKey: gitQueryKeys.status(cwd),48 queryKey: gitQueryKeys.status(cwd),