Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { workspaceTrpc } from "@superset/workspace-client";
import { useCallback } from "react";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { useWorkspaceEvent } from "../useWorkspaceEvent";

/**
Expand All @@ -15,20 +14,25 @@ import { useWorkspaceEvent } from "../useWorkspaceEvent";
* debounce needed.
*/
export function useGitStatus(workspaceId: string) {
const collections = useCollections();
const baseBranch: string | null =
collections.v2WorkspaceLocalState.get(workspaceId)?.sidebarState
?.baseBranch ?? null;

const utils = workspaceTrpc.useUtils();

const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery(
{ workspaceId },
{ staleTime: Number.POSITIVE_INFINITY, enabled: Boolean(workspaceId) },
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
);
const baseBranch = baseBranchQuery.data?.baseBranch ?? null;

const query = workspaceTrpc.git.getStatus.useQuery(
{ workspaceId, baseBranch: baseBranch ?? undefined },
{ refetchOnWindowFocus: true, enabled: Boolean(workspaceId) },
);

const invalidate = useCallback(() => {
void utils.git.getStatus.invalidate({ workspaceId });
// Current branch may have changed (external checkout), and
// branch.<name>.base is per-branch — drop the cache so the next read
// picks up the new branch's base.
void utils.git.getBaseBranch.invalidate({ workspaceId });
}, [utils, workspaceId]);

useWorkspaceEvent("git:changed", workspaceId, invalidate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CommitFilterDropdown } from "../CommitFilterDropdown";
interface ChangesHeaderProps {
currentBranch: { name: string; aheadCount: number; behindCount: number };
defaultBranchName: string;
baseBranch: string | null;
commitCount: number;
totalFiles: number;
totalAdditions: number;
Expand All @@ -25,6 +26,7 @@ interface ChangesHeaderProps {
export function ChangesHeader({
currentBranch,
defaultBranchName,
baseBranch,
commitCount,
totalFiles,
totalAdditions,
Expand Down Expand Up @@ -103,7 +105,7 @@ export function ChangesHeader({
{commitCount} {commitCount === 1 ? "commit" : "commits"} from{" "}
<BaseBranchSelector
branches={branches}
currentValue={defaultBranchName}
currentValue={baseBranch ?? defaultBranchName}
onChange={onBaseBranchChange}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ChangesTabContentProps {
commits: { data: RouterOutputs["git"]["listCommits"] | undefined };
branches: { data: RouterOutputs["git"]["listBranches"] | undefined };
filter: ChangesFilter;
baseBranch: string | null;
files: ChangesetFile[];
isLoading: boolean;
totalChanges: number;
Expand All @@ -35,6 +36,7 @@ export const ChangesTabContent = memo(function ChangesTabContent({
commits,
branches,
filter,
baseBranch,
files,
isLoading,
totalChanges,
Expand Down Expand Up @@ -69,6 +71,7 @@ export const ChangesTabContent = memo(function ChangesTabContent({
<ChangesHeader
currentBranch={status.data.currentBranch}
defaultBranchName={status.data.defaultBranch.name}
baseBranch={baseBranch}
commitCount={commits.data?.commits.length ?? 0}
totalFiles={totalChanges}
totalAdditions={totalAdditions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ export function useChangesTab({
onSelectFile,
}: UseChangesTabParams): SidebarTabDefinition {
const collections = useCollections();
const utils = workspaceTrpc.useUtils();
const localState = collections.v2WorkspaceLocalState.get(workspaceId);
const filter: ChangesFilter = localState?.sidebarState?.changesFilter ?? {
kind: "all",
};
const baseBranch: string | null =
localState?.sidebarState?.baseBranch ?? null;

const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery(
{ workspaceId },
{ staleTime: Number.POSITIVE_INFINITY },
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: getBaseBranch is cached forever here, but this value depends on the current checked-out branch and is not invalidated on general git change events. That can leave the sidebar/commit comparison using an outdated base branch after branch switches.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx, line 35:

<comment>`getBaseBranch` is cached forever here, but this value depends on the current checked-out branch and is not invalidated on general git change events. That can leave the sidebar/commit comparison using an outdated base branch after branch switches.</comment>

<file context>
@@ -24,12 +24,17 @@ export function useChangesTab({
+
+	const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery(
+		{ workspaceId },
+		{ staleTime: Number.POSITIVE_INFINITY },
+	);
+	const baseBranch = baseBranchQuery.data?.baseBranch ?? null;
</file context>
Fix with Cubic

);
const baseBranch = baseBranchQuery.data?.baseBranch ?? null;

const { viewedSet, setViewed } = useViewedFiles(workspaceId);

Expand All @@ -46,14 +51,20 @@ export function useChangesTab({
[collections, workspaceId],
);

const setBaseBranchMutation = workspaceTrpc.git.setBaseBranch.useMutation({
onSuccess: () => {
void utils.git.getBaseBranch.invalidate({ workspaceId });
void utils.git.getStatus.invalidate({ workspaceId });
void utils.git.listCommits.invalidate({ workspaceId });
void utils.git.getDiff.invalidate({ workspaceId });
},
});

const setBaseBranch = useCallback(
(branchName: string) => {
if (!collections.v2WorkspaceLocalState.get(workspaceId)) return;
collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
draft.sidebarState.baseBranch = branchName;
});
setBaseBranchMutation.mutate({ workspaceId, baseBranch: branchName });
},
[collections, workspaceId],
[setBaseBranchMutation, workspaceId],
);

const commits = workspaceTrpc.git.listCommits.useQuery(
Expand Down Expand Up @@ -101,6 +112,7 @@ export function useChangesTab({
commits={commits}
branches={branches}
filter={filter}
baseBranch={baseBranch}
files={files}
isLoading={isLoading}
totalChanges={totalChanges}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { workspaceTrpc } from "@superset/workspace-client";
import { eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
import { useMemo } from "react";
Expand All @@ -15,7 +16,12 @@ export function useSidebarDiffRef(workspaceId: string): DiffRef {
);
const sidebarState = rows[0]?.sidebarState;
const filter = sidebarState?.changesFilter ?? { kind: "all" };
const baseBranch = sidebarState?.baseBranch ?? null;

const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery(
{ workspaceId },
{ staleTime: Number.POSITIVE_INFINITY },
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Caching git.getBaseBranch with infinite staleness can keep an outdated base branch after checkout changes, so sidebar diffs may compare against the wrong base.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useSidebarDiffRef/useSidebarDiffRef.ts, line 22:

<comment>Caching `git.getBaseBranch` with infinite staleness can keep an outdated base branch after checkout changes, so sidebar diffs may compare against the wrong base.</comment>

<file context>
@@ -15,7 +16,12 @@ export function useSidebarDiffRef(workspaceId: string): DiffRef {
+
+	const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery(
+		{ workspaceId },
+		{ staleTime: Number.POSITIVE_INFINITY },
+	);
+	const baseBranch = baseBranchQuery.data?.baseBranch ?? null;
</file context>
Fix with Cubic

);
const baseBranch = baseBranchQuery.data?.baseBranch ?? null;

const filterKind = filter.kind;
const commitHash =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const workspaceLocalStateSchema = z.object({
tabOrder: z.number().int().default(0),
sectionId: z.string().uuid().nullable().default(null),
changesFilter: changesFilterSchema.default({ kind: "all" }),
baseBranch: z.string().nullable().default(null),
}),
paneLayout: paneWorkspaceStateSchema,
rightSidebarOpen: z.boolean().default(false),
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions packages/host-service/src/trpc/router/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,58 @@ export const gitRouter = router({
return { files };
}),

getBaseBranch: protectedProcedure
.input(z.object({ workspaceId: z.string() }))
.query(async ({ ctx, input }) => {
const worktreePath = resolveWorktreePath(ctx, input.workspaceId);
const git = await ctx.git(worktreePath);
const currentBranch = (
await git.revparse(["--abbrev-ref", "HEAD"]).catch(() => "")
).trim();
if (!currentBranch || currentBranch === "HEAD") {
return { baseBranch: null as string | null };
}
const configured = (
await git
.raw(["config", `branch.${currentBranch}.base`])
.catch(() => "")
).trim();
return { baseBranch: (configured || null) as string | null };
}),

setBaseBranch: protectedProcedure
.input(
z.object({
workspaceId: z.string(),
baseBranch: z.string().nullable(),
}),
)
.mutation(async ({ ctx, input }) => {
const worktreePath = resolveWorktreePath(ctx, input.workspaceId);
const git = await ctx.git(worktreePath);
const currentBranch = (
await git.revparse(["--abbrev-ref", "HEAD"]).catch(() => "")
).trim();
if (!currentBranch || currentBranch === "HEAD") {
throw new TRPCError({
code: "PRECONDITION_FAILED",
message: "Cannot set base branch on detached HEAD",
});
}
if (input.baseBranch) {
await git.raw([
"config",
`branch.${currentBranch}.base`,
input.baseBranch,
]);
} else {
await git
.raw(["config", "--unset", `branch.${currentBranch}.base`])
.catch(() => {});
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not swallow errors when unsetting the base branch config; this can report success even when persistence fails.

(Based on your team's feedback about handling async errors explicitly and avoiding silent empty catches.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/trpc/router/git/git.ts, line 281:

<comment>Do not swallow errors when unsetting the base branch config; this can report success even when persistence fails.

(Based on your team's feedback about handling async errors explicitly and avoiding silent empty catches.) </comment>

<file context>
@@ -231,6 +231,58 @@ export const gitRouter = router({
+			} else {
+				await git
+					.raw(["config", "--unset", `branch.${currentBranch}.base`])
+					.catch(() => {});
+			}
+			return { baseBranch: input.baseBranch };
</file context>
Suggested change
.catch(() => {});
.catch((error) => {
if (`${error}`.includes("No such section or key")) return;
throw error;
});
Fix with Cubic

}
return { baseBranch: input.baseBranch };
}),

renameBranch: protectedProcedure
.input(
z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,8 +639,9 @@ export const workspaceCreationRouter = router({
// Always create a new branch — never check out an existing one.
// Checking out existing branches is a separate intent (createFromPr,
// or the picker's Check out action via the `checkout` procedure).
// --no-track prevents the new branch from tracking the remote ref
// (e.g. origin/main); push.autoSetupRemote handles first-push tracking.
// --no-track keeps `git pull` / ahead-behind counts from treating
// the start point as the branch's home. Push targeting is handled
// separately by push.autoSetupRemote (set below).
const startPointArg =
startPoint.kind === "head" ? "HEAD" : startPoint.shortName;
await git.raw([
Expand All @@ -655,6 +656,45 @@ export const workspaceCreationRouter = router({
: startPointArg,
]);

// Enable autoSetupRemote so the first terminal `git push` creates
// origin/<branchName> and sets it as upstream without requiring
// `-u`. Note: `--local` in a linked worktree writes to the shared
// repo config, so this applies repo-wide — intentional, every
// workspace worktree wants the same ergonomics. Safe against
// wrong-upstream targeting because --no-track above guarantees no
// upstream exists at first push, so auto-create always wins and
// always uses the branch's own name (never the base branch).
await git
.raw([
"-C",
worktreePath,
"config",
"--local",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"push.autoSetupRemote",
"true",
])
.catch((err) => {
console.warn(
"[workspaceCreation.create] failed to set push.autoSetupRemote:",
err,
);
});

// Record the base branch in git config so the Changes tab knows what
// to compare against on first open. startPoint.shortName is the ref
// we actually forked from (user selection, resolved against local /
// remote). Skipped for "head" start point — no meaningful base.
if (startPoint.kind !== "head") {
await git
.raw(["config", `branch.${branchName}.base`, startPoint.shortName])
.catch((err) => {
console.warn(
`[workspaceCreation.create] failed to record base branch ${startPoint.shortName}:`,
err,
);
});
}

setProgress(input.pendingId, "registering");

// 4. Register cloud workspace row
Expand Down Expand Up @@ -909,6 +949,28 @@ export const workspaceCreationRouter = router({
throw new TRPCError({ code: "CONFLICT", message });
}

// Enable autoSetupRemote so the first terminal `git push` on a
// local-only branch creates origin/<branch> without requiring -u.
// Branches checked out from a remote already have upstream set
// via --track above, so this config is a no-op for them.
// `--local` in a linked worktree writes to the shared repo config,
// so this applies repo-wide — intentional.
await git
.raw([
"-C",
worktreePath,
"config",
"--local",
"push.autoSetupRemote",
"true",
])
.catch((err) => {
console.warn(
"[workspaceCreation.checkout] failed to set push.autoSetupRemote:",
err,
);
});

setProgress(input.pendingId, "registering");

const rollbackWorktree = async () => {
Expand Down
Loading