From c10cd46d425b83ecae074e27231133ce47df4809 Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Thu, 14 May 2026 19:34:49 +0530 Subject: [PATCH 1/2] feat: git status with primary and current deployment branches --- .../edit-session/PublishPopover.svelte | 22 +++---- .../src/features/edit-session/selectors.ts | 66 +++++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 web-admin/src/features/edit-session/selectors.ts diff --git a/web-admin/src/features/edit-session/PublishPopover.svelte b/web-admin/src/features/edit-session/PublishPopover.svelte index fc7b670b4bb..10b5f232845 100644 --- a/web-admin/src/features/edit-session/PublishPopover.svelte +++ b/web-admin/src/features/edit-session/PublishPopover.svelte @@ -17,13 +17,13 @@ import { createRuntimeServiceGitMergeToBranchMutation, createRuntimeServiceGitPushMutation, - createRuntimeServiceGitStatus, getRuntimeServiceGitStatusQueryKey, } from "@rilldata/web-common/runtime-client"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { Rocket } from "lucide-svelte"; import { buildPostMergeUrl } from "./post-merge-url"; import { goto } from "$app/navigation"; + import { getDeploymentGithubStatus } from "@rilldata/web-admin/features/edit-session/selectors.ts"; export let organization: string; export let project: string; @@ -36,7 +36,7 @@ const client = useRuntimeClient(); const gitPushMutation = createRuntimeServiceGitPushMutation(client); const gitMergeMutation = createRuntimeServiceGitMergeToBranchMutation(client); - const gitStatusQuery = createRuntimeServiceGitStatus(client, {}); + const gitStatusQuery = getDeploymentGithubStatus(client, primaryBranch); // Query GetProject without a branch param so `data.deployment` reflects // the project's primary (prod) deployment — the same source of truth the // project layout uses. ListDeployments is too loose: it includes orphan @@ -44,21 +44,17 @@ const projectQuery = createAdminServiceGetProject(organization, project); const redeployProjectMutation = createAdminServiceRedeployProject(); - $: currentBranch = $gitStatusQuery.data?.branch ?? ""; - $: hasLocalChanges = $gitStatusQuery.data?.localChanges ?? false; + $: ({ + isPending, + data: { hasLocalChanges, alreadyOnPrimary, disabledPerGitStatus }, + } = $gitStatusQuery); + $: projectLoaded = $projectQuery.data !== undefined; $: prodDeployment = $projectQuery.data?.deployment; $: prodDeploymentActive = !!prodDeployment && isActiveDeployment(prodDeployment); - $: alreadyOnPrimary = - !!primaryBranch && !!currentBranch && currentBranch === primaryBranch; // TODO: this should also check currentBranch vs primaryBranch once that API is available. - $: disabled = - !primaryBranch || - !currentBranch || - !projectLoaded || - alreadyOnPrimary || - isPublishing; + $: disabled = !projectLoaded || disabledPerGitStatus || isPublishing; // Prefetch prod's project parser commit SHA so the deploying page can // wait for prod to advance past it before redirecting to the dashboard, @@ -193,7 +189,7 @@ {#if alreadyOnPrimary} Already on production - {:else if !primaryBranch || !currentBranch || !projectLoaded} + {:else if isPending || !projectLoaded} Loading project... {:else if !hasLocalChanges} No changes to publish diff --git a/web-admin/src/features/edit-session/selectors.ts b/web-admin/src/features/edit-session/selectors.ts new file mode 100644 index 00000000000..8f46702c111 --- /dev/null +++ b/web-admin/src/features/edit-session/selectors.ts @@ -0,0 +1,66 @@ +import { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; +import { derived } from "svelte/store"; +import { createRuntimeServiceGitStatus } from "@rilldata/web-common/runtime-client"; + +export function getDeploymentGithubStatus( + runtimeClient: RuntimeClient, + primaryBranch: string | undefined, +) { + return derived( + [ + createRuntimeServiceGitStatus(runtimeClient, {}), + createRuntimeServiceGitStatus(runtimeClient, { + remoteBranch: primaryBranch, + }), + ], + ([currentBranchGitStatusResp, primaryBranchGitStatusResp]) => { + const isPending = + currentBranchGitStatusResp.isPending || + primaryBranchGitStatusResp.isPending; + const error = + currentBranchGitStatusResp.error || primaryBranchGitStatusResp.error; + if (isPending || error) { + return { + isPending, + error, + data: { + hasLocalChanges: false, + alreadyOnPrimary: false, + disabledPerGitStatus: true, + }, + }; + } + + const currentBranch = currentBranchGitStatusResp.data?.branch ?? ""; + const hasChangesAgainstCurrent = Boolean( + currentBranchGitStatusResp.data?.localCommits || + currentBranchGitStatusResp.data?.localChanges, + ); + const hasChangesAgainstPrimary = Boolean( + primaryBranchGitStatusResp.data?.localCommits || + primaryBranchGitStatusResp.data?.localChanges, + ); + const hasLocalChanges = + hasChangesAgainstCurrent || hasChangesAgainstPrimary; + + const alreadyOnPrimary = + !!primaryBranch && !!currentBranch && currentBranch === primaryBranch; + + const disabledPerGitStatus = + !primaryBranch || + !currentBranch || + alreadyOnPrimary || + !hasLocalChanges; + + return { + isPending: false, + error: undefined, + data: { + hasLocalChanges, + alreadyOnPrimary, + disabledPerGitStatus, + }, + }; + }, + ); +} From 1e8cea9c4e9e5842865c16ceeb9850d0a8801b6c Mon Sep 17 00:00:00 2001 From: Aditya Hegde Date: Fri, 15 May 2026 08:57:25 +0530 Subject: [PATCH 2/2] Optimise GitStatus calls --- .../edit-session/PublishPopover.svelte | 29 +++++++++++-- .../src/features/edit-session/selectors.ts | 43 +++++++++++++++++-- .../invalidation/file-invalidators.spec.ts | 10 +++-- .../invalidation/file-invalidators.ts | 19 ++++++-- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/web-admin/src/features/edit-session/PublishPopover.svelte b/web-admin/src/features/edit-session/PublishPopover.svelte index 10b5f232845..6c16acbddb6 100644 --- a/web-admin/src/features/edit-session/PublishPopover.svelte +++ b/web-admin/src/features/edit-session/PublishPopover.svelte @@ -23,7 +23,10 @@ import { Rocket } from "lucide-svelte"; import { buildPostMergeUrl } from "./post-merge-url"; import { goto } from "$app/navigation"; - import { getDeploymentGithubStatus } from "@rilldata/web-admin/features/edit-session/selectors.ts"; + import { + fetchDeploymentGithubStatusChanges, + getDeploymentGithubStatus, + } from "@rilldata/web-admin/features/edit-session/selectors.ts"; export let organization: string; export let project: string; @@ -46,14 +49,18 @@ $: ({ isPending, - data: { hasLocalChanges, alreadyOnPrimary, disabledPerGitStatus }, + data: { + hasLocalChanges, + hasChangesOnCurrent, + alreadyOnPrimary, + disabledPerGitStatus, + }, } = $gitStatusQuery); $: projectLoaded = $projectQuery.data !== undefined; $: prodDeployment = $projectQuery.data?.deployment; $: prodDeploymentActive = !!prodDeployment && isActiveDeployment(prodDeployment); - // TODO: this should also check currentBranch vs primaryBranch once that API is available. $: disabled = !projectLoaded || disabledPerGitStatus || isPublishing; // Prefetch prod's project parser commit SHA so the deploying page can @@ -90,6 +97,22 @@ {}, ); + // Refetch local changes status, we predict this based on file watcher response. + // But we dont check if changes flipped to with changes to without changes. + hasLocalChanges = await fetchDeploymentGithubStatusChanges( + client, + queryClient, + primaryBranch, + ); + if (!hasLocalChanges && !hasChangesOnCurrent) { + eventBus.emit("notification", { + type: "default", + message: "No changes detected", + }); + isPublishing = false; + return; + } + try { if (hasLocalChanges) { await $gitPushMutation.mutateAsync({ diff --git a/web-admin/src/features/edit-session/selectors.ts b/web-admin/src/features/edit-session/selectors.ts index 8f46702c111..d811e3e4eb1 100644 --- a/web-admin/src/features/edit-session/selectors.ts +++ b/web-admin/src/features/edit-session/selectors.ts @@ -1,6 +1,12 @@ import { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { derived } from "svelte/store"; -import { createRuntimeServiceGitStatus } from "@rilldata/web-common/runtime-client"; +import { + createRuntimeServiceGitStatus, + getRuntimeServiceGitStatusQueryKey, + runtimeServiceGitStatus, + type V1GitStatusResponse, +} from "@rilldata/web-common/runtime-client"; +import type { QueryClient } from "@tanstack/query-core"; export function getDeploymentGithubStatus( runtimeClient: RuntimeClient, @@ -25,6 +31,7 @@ export function getDeploymentGithubStatus( error, data: { hasLocalChanges: false, + hasChangesOnCurrent: false, alreadyOnPrimary: false, disabledPerGitStatus: true, }, @@ -36,12 +43,11 @@ export function getDeploymentGithubStatus( currentBranchGitStatusResp.data?.localCommits || currentBranchGitStatusResp.data?.localChanges, ); - const hasChangesAgainstPrimary = Boolean( + const hasChangesOnCurrent = Boolean( primaryBranchGitStatusResp.data?.localCommits || primaryBranchGitStatusResp.data?.localChanges, ); - const hasLocalChanges = - hasChangesAgainstCurrent || hasChangesAgainstPrimary; + const hasLocalChanges = hasChangesAgainstCurrent || hasChangesOnCurrent; const alreadyOnPrimary = !!primaryBranch && !!currentBranch && currentBranch === primaryBranch; @@ -57,6 +63,7 @@ export function getDeploymentGithubStatus( error: undefined, data: { hasLocalChanges, + hasChangesOnCurrent, alreadyOnPrimary, disabledPerGitStatus, }, @@ -64,3 +71,31 @@ export function getDeploymentGithubStatus( }, ); } + +export async function fetchDeploymentGithubStatusChanges( + runtimeClient: RuntimeClient, + queryClient: QueryClient, + primaryBranch: string | undefined, +) { + const currentBranchGitStatusResp = await queryClient.fetchQuery({ + queryKey: getRuntimeServiceGitStatusQueryKey(runtimeClient.instanceId, {}), + queryFn: () => runtimeServiceGitStatus(runtimeClient, {}), + }); + const hasChangesAgainstCurrent = Boolean( + currentBranchGitStatusResp.localCommits || + currentBranchGitStatusResp.localChanges, + ); + + const primaryBranchGitStatusResp = + queryClient.getQueryData( + getRuntimeServiceGitStatusQueryKey(runtimeClient.instanceId, { + remoteBranch: primaryBranch, + }), + ); + const hasChangesAgainstPrimary = Boolean( + primaryBranchGitStatusResp?.localCommits || + primaryBranchGitStatusResp?.localChanges, + ); + + return hasChangesAgainstCurrent || hasChangesAgainstPrimary; +} diff --git a/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts b/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts index e6c2fb278a9..f238ab2f5f4 100644 --- a/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts +++ b/web-common/src/runtime-client/invalidation/file-invalidators.spec.ts @@ -48,10 +48,12 @@ function fakeQueryClient() { invalidateQueries: vi.fn(), refetchQueries: vi.fn(), resetQueries: vi.fn(), + getQueryData: vi.fn(), } as unknown as QueryClient & { invalidateQueries: ReturnType; refetchQueries: ReturnType; resetQueries: ReturnType; + getQueryData: ReturnType; }; } @@ -229,10 +231,10 @@ describe("handleFileEvent", () => { ); const gitStatusKey = getRuntimeServiceGitStatusQueryKey(INSTANCE_ID, {}); - const gitHit = qc.invalidateQueries.mock.calls.some( - ([arg]) => - Array.isArray(arg.queryKey) && - JSON.stringify(arg.queryKey) === JSON.stringify(gitStatusKey), + const gitHit = qc.getQueryData.mock.calls.some( + ([queryKey]) => + Array.isArray(queryKey) && + JSON.stringify(queryKey) === JSON.stringify(gitStatusKey), ); expect(gitHit).toBe(true); }); diff --git a/web-common/src/runtime-client/invalidation/file-invalidators.ts b/web-common/src/runtime-client/invalidation/file-invalidators.ts index 436f66d4e72..9294ea875f8 100644 --- a/web-common/src/runtime-client/invalidation/file-invalidators.ts +++ b/web-common/src/runtime-client/invalidation/file-invalidators.ts @@ -9,6 +9,7 @@ import { getRuntimeServiceIssueDevJWTQueryKey, getRuntimeServiceListFilesQueryKey, V1FileEvent, + type V1GitStatusResponse, type V1WatchFilesResponse, } from "@rilldata/web-common/runtime-client"; import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; @@ -80,9 +81,7 @@ export async function handleFileEvent( } // Keep the cloud editor's commit button in sync with the working tree. - void queryClient.invalidateQueries({ - queryKey: getRuntimeServiceGitStatusQueryKey(instanceId, {}), - }); + resetGitStatusQuery(queryClient, instanceId); } // Throttle: when many files arrive at once (e.g. initial sync), one refetch @@ -95,3 +94,17 @@ export async function handleFileEvent( ); } } + +function resetGitStatusQuery(queryClient: QueryClient, instanceId: string) { + const queryKey = getRuntimeServiceGitStatusQueryKey(instanceId, {}); + + const existingQueryData = + queryClient.getQueryData(queryKey); + // Skip updating the cache if there is no query data. + if (!existingQueryData) return; + + queryClient.setQueryData(queryKey, { + ...existingQueryData, + localChanges: true, // Force localChanges=true + }); +}