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 {
4747 gitPullMutationOptions,
4848 gitRunStackedActionMutationOptions,
4949 gitStatusQueryOptions,
50 invalidateGitStatusQuery,
5051 invalidateGitQueries,
5152} from "~/lib/gitReactQuery";
5253import { newCommandId, randomUUID } from "~/lib/utils";
......@@ -286,8 +287,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
286287
287288 useEffect(() => {
288289 if (!isGitStatusOutOfSync) return;
289 void invalidateGitQueries(queryClient);
290 }, [isGitStatusOutOfSync, queryClient]);
290 void invalidateGitQueries(queryClient, { cwd: gitCwd });
291 }, [gitCwd, isGitStatusOutOfSync, queryClient]);
291292
292293 const gitStatusForActions = isGitStatusOutOfSync ? null : gitStatus;
293294
......@@ -814,7 +815,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
814815 <GroupSeparator className="hidden @3xl/header-actions:block" />
815816 <Menu
816817 onOpenChange={(open) => {
817 if (open) void invalidateGitQueries(queryClient);
818 if (open) void invalidateGitStatusQuery(queryClient, gitCwd);
818819 }}
819820 >
820821 <MenuTrigger
apps/web/src/lib/gitReactQuery.test.ts+63-1
......@@ -1,10 +1,24 @@
11import { 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
312import {
13 gitBranchesQueryOptions,
414 gitMutationKeys,
15 gitQueryKeys,
516 gitPreparePullRequestThreadMutationOptions,
617 gitPullMutationOptions,
718 gitRunStackedActionMutationOptions,
19 invalidateGitStatusQuery,
20 gitStatusQueryOptions,
21 invalidateGitQueries,
822} from "./gitReactQuery";
923
1024describe("gitMutationKeys", () => {
......@@ -49,3 +63,51 @@ describe("git mutation options", () => {
4963 expect(options.mutationKey).toEqual(gitMutationKeys.preparePullRequestThread("/repo/a"));
5064 });
5165});
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 = {
2323 ["git", "mutation", "prepare-pull-request-thread", cwd] as const,
2424};
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
2735 return queryClient.invalidateQueries({ queryKey: gitQueryKeys.all });
2836}
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
3046export function gitStatusQueryOptions(cwd: string | null) {
3147 return queryOptions({
3248 queryKey: gitQueryKeys.status(cwd),