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
82 changes: 82 additions & 0 deletions apps/desktop/src/lib/trpc/routers/changes/status.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { projects, workspaces, worktrees } from "@superset/local-db";
import { and, eq, isNull, not } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import type { ChangedFile, GitChangesStatus } from "shared/changes-types";
import simpleGit from "simple-git";
import { z } from "zod";
Expand Down Expand Up @@ -30,6 +33,10 @@ export const createStatusRouter = () => {
// Use --no-optional-locks to avoid holding locks on the repository
const status = await getStatusNoLock(input.worktreePath);
const parsed = parseGitStatus(status);
syncWorkspaceBranch({
worktreePath: input.worktreePath,
currentBranch: parsed.branch,
});

// Run independent operations in parallel
const [branchComparison, trackingStatus] = await Promise.all([
Expand Down Expand Up @@ -94,6 +101,81 @@ export const createStatusRouter = () => {
});
};

/**
* Update local DB branch fields to match the current git branch for a worktree
* or main repo workspace path.
*/
function syncWorkspaceBranch({
worktreePath,
currentBranch,
}: {
worktreePath: string;
currentBranch: string;
}): void {
if (!currentBranch || currentBranch === "HEAD") {
return;
}

try {
const worktree = localDb
.select({ id: worktrees.id })
.from(worktrees)
.where(eq(worktrees.path, worktreePath))
.get();

if (worktree) {
localDb
.update(worktrees)
.set({ branch: currentBranch })
.where(
and(
eq(worktrees.id, worktree.id),
not(eq(worktrees.branch, currentBranch)),
),
)
.run();

localDb
.update(workspaces)
.set({ branch: currentBranch })
.where(
and(
eq(workspaces.worktreeId, worktree.id),
isNull(workspaces.deletingAt),
not(eq(workspaces.branch, currentBranch)),
),
)
.run();

return;
}

const project = localDb
.select({ id: projects.id })
.from(projects)
.where(eq(projects.mainRepoPath, worktreePath))
.get();
if (!project) {
return;
}

localDb
.update(workspaces)
.set({ branch: currentBranch })
.where(
and(
eq(workspaces.projectId, project.id),
eq(workspaces.type, "branch"),
isNull(workspaces.deletingAt),
not(eq(workspaces.branch, currentBranch)),
),
)
.run();
} catch (error) {
console.warn("[changes/status] Failed to sync branch:", error);
}
}

interface BranchComparison {
commits: GitChangesStatus["commits"];
againstBase: ChangedFile[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,11 @@ export const createGitStatusProcedures = () => {

// Extract worktree name from path (last segment)
const worktreeName = worktree.path.split("/").pop() ?? worktree.branch;
const branchName = worktree.branch;

return {
worktreeName,
branchName,
createdAt: worktree.createdAt,
gitStatus: worktree.gitStatus ?? null,
githubStatus: worktree.githubStatus ?? null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation";
import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner";
import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator";
import { useBranchSyncInvalidation } from "renderer/screens/main/hooks/useBranchSyncInvalidation";
import { useWorkspaceRename } from "renderer/screens/main/hooks/useWorkspaceRename";
import { useTabsStore } from "renderer/stores/tabs/store";
import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils";
Expand Down Expand Up @@ -136,6 +137,12 @@ export function WorkspaceListItem({
},
);

useBranchSyncInvalidation({
gitBranch: localChanges?.branch,
workspaceBranch: branch,
workspaceId: id,
});

// Calculate total local changes (staged + unstaged + untracked)
const localDiffStats = useMemo(() => {
if (!localChanges) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function WorkspaceHoverCardContent({
const needsRebase = worktreeInfo?.gitStatus?.needsRebase;

const worktreeName = worktreeInfo?.worktreeName;
const branchName = worktreeInfo?.branchName;
const hasCustomAlias =
workspaceAlias && worktreeName && workspaceAlias !== worktreeName;

Expand All @@ -49,19 +50,19 @@ export function WorkspaceHoverCardContent({
{hasCustomAlias && (
<div className="text-sm font-medium">{workspaceAlias}</div>
)}
{worktreeName && (
{branchName && (
<div className="space-y-0.5">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">
Branch
</span>
{repoUrl && branchExistsOnRemote ? (
<a
href={`${repoUrl}/tree/${worktreeName}`}
href={`${repoUrl}/tree/${branchName}`}
target="_blank"
rel="noopener noreferrer"
className={`flex items-center gap-1 font-mono break-all hover:underline ${hasCustomAlias ? "text-xs" : "text-sm"}`}
>
{worktreeName}
{branchName}
<LuExternalLink
className="size-3 shrink-0"
strokeWidth={STROKE_WIDTH}
Expand All @@ -71,7 +72,7 @@ export function WorkspaceHoverCardContent({
<code
className={`font-mono break-all block ${hasCustomAlias ? "text-xs" : "text-sm"}`}
>
{worktreeName}
{branchName}
</code>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react";
import { HiMiniMinus, HiMiniPlus } from "react-icons/hi2";
import { LuUndo2 } from "react-icons/lu";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useBranchSyncInvalidation } from "renderer/screens/main/hooks/useBranchSyncInvalidation";
import { useChangesStore } from "renderer/stores/changes";
import type { ChangeCategory, ChangedFile } from "shared/changes-types";
import { CategorySection } from "./components/CategorySection";
Expand Down Expand Up @@ -69,6 +70,12 @@ export function ChangesView({ onFileOpen, isExpandedView }: ChangesViewProps) {
},
);

useBranchSyncInvalidation({
gitBranch: status?.branch,
workspaceBranch: workspace?.branch,
workspaceId: workspaceId ?? "",
});

const handleRefresh = () => {
refetch();
refetchGithubStatus();
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/screens/main/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { useBranchSyncInvalidation } from "./useBranchSyncInvalidation";
export { usePRStatus } from "./usePRStatus";
export { useWorkspaceRename } from "./useWorkspaceRename";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useBranchSyncInvalidation } from "./useBranchSyncInvalidation";
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";

/**
* Invalidates workspace-related caches when the git branch (from status polling)
* diverges from the branch stored in the local DB workspace record.
*
* This keeps sidebar labels and hover cards in sync after external `git switch`.
*/
export function useBranchSyncInvalidation({
gitBranch,
workspaceBranch,
workspaceId,
}: {
gitBranch: string | undefined;
workspaceBranch: string | undefined;
workspaceId: string;
}) {
const utils = electronTrpc.useUtils();

useEffect(() => {
if (!gitBranch || gitBranch === "HEAD" || !workspaceBranch) return;
if (gitBranch !== workspaceBranch) {
utils.workspaces.getAllGrouped.invalidate();
utils.workspaces.get.invalidate({ id: workspaceId });
utils.workspaces.getWorktreeInfo.invalidate({ workspaceId });
}
}, [gitBranch, workspaceBranch, workspaceId, utils]);
}