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
Expand Up @@ -732,6 +732,8 @@ export const createCreateProcedures = () => {
gitStatus: {
branch: existingWorktree.branch,
needsRebase: false,
ahead: 0,
behind: 0,
lastRefreshed: Date.now(),
},
})
Expand Down Expand Up @@ -816,6 +818,8 @@ export const createCreateProcedures = () => {
gitStatus: {
branch: input.branch,
needsRebase: false,
ahead: 0,
behind: 0,
lastRefreshed: Date.now(),
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
updateProjectDefaultBranch,
} from "../utils/db-helpers";
import {
checkNeedsRebase,
fetchDefaultBranch,
getAheadBehindCount,
getDefaultBranch,
listExternalWorktrees,
refreshDefaultBranch,
Expand Down Expand Up @@ -42,7 +42,6 @@ export const createGitStatusProcedures = () => {
throw new Error(`Project ${workspace.projectId} not found`);
}

// Sync with remote in case the default branch changed (e.g. master -> main)
const remoteDefaultBranch = await refreshDefaultBranch(
project.mainRepoPath,
);
Expand All @@ -59,22 +58,21 @@ export const createGitStatusProcedures = () => {
updateProjectDefaultBranch(project.id, defaultBranch);
}

// Fetch default branch to get latest
await fetchDefaultBranch(project.mainRepoPath, defaultBranch);

// Check if worktree branch is behind origin/{defaultBranch}
const needsRebase = await checkNeedsRebase(
worktree.path,
const { ahead, behind } = await getAheadBehindCount({
repoPath: worktree.path,
defaultBranch,
);
});

const gitStatus = {
branch: worktree.branch,
needsRebase,
needsRebase: behind > 0,
ahead,
behind,
lastRefreshed: Date.now(),
};

// Update worktree in db
localDb
.update(worktrees)
.set({ gitStatus })
Expand All @@ -84,6 +82,25 @@ export const createGitStatusProcedures = () => {
return { gitStatus, defaultBranch };
}),

getAheadBehind: publicProcedure
.input(z.object({ workspaceId: z.string() }))
.query(async ({ input }) => {
const workspace = getWorkspace(input.workspaceId);
if (!workspace) {
return { ahead: 0, behind: 0 };
}

const project = getProject(workspace.projectId);
if (!project) {
return { ahead: 0, behind: 0 };
}

return getAheadBehindCount({
repoPath: project.mainRepoPath,
defaultBranch: workspace.branch,
});
}),
Comment on lines +85 to +102
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Verify that getAheadBehind semantics match the intended use for branch workspaces.

The procedure compares origin/{workspace.branch}...HEAD in the main repo. This correctly shows how the local branch relates to its remote tracking branch — but only when the main repo's HEAD is on workspace.branch. If the user switches branches externally (e.g., via terminal), HEAD will refer to a different branch and the numbers will be misleading.

Consider using an explicit branch ref instead of implicit HEAD to make this resilient:

Proposed fix

In getAheadBehindCount (or a variant), replace HEAD with the explicit branch name:

 const output = await git.raw([
   "rev-list",
   "--left-right",
   "--count",
-  `origin/${defaultBranch}...HEAD`,
+  `origin/${defaultBranch}...${defaultBranch}`,
 ]);

This would require either a separate parameter or passing the local branch name explicitly. The tradeoff is that refreshGitStatus (for worktrees) correctly uses HEAD since the worktree's HEAD is always on its branch.

🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts` around
lines 85 - 102, The getAheadBehind procedure calls getAheadBehindCount with
repoPath: project.mainRepoPath and defaultBranch: workspace.branch but
getAheadBehindCount currently compares origin/{branch}...HEAD which is only
correct when the main repo's HEAD is on workspace.branch; change the call and/or
getAheadBehindCount signature to accept an explicit localBranch ref (e.g.,
localBranch: workspace.branch) and have getAheadBehindCount compare
origin/{branch}...refs/heads/{localBranch} (or otherwise use the explicit branch
ref instead of implicit HEAD) so the ahead/behind numbers reflect the specified
workspace.branch; keep refreshGitStatus behavior as-is since worktrees rely on
HEAD.


getGitHubStatus: publicProcedure
.input(z.object({ workspaceId: z.string() }))
.query(async ({ input }) => {
Expand All @@ -99,10 +116,8 @@ export const createGitStatusProcedures = () => {
return null;
}

// Always fetch fresh data on hover
const freshStatus = await fetchGitHubPRStatus(worktree.path);

// Update cache if we got data
if (freshStatus) {
localDb
.update(worktrees)
Expand All @@ -129,7 +144,6 @@ export const createGitStatusProcedures = () => {
return null;
}

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

Expand Down
25 changes: 25 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,31 @@ export async function checkNeedsRebase(
return Number.parseInt(behindCount.trim(), 10) > 0;
}

export async function getAheadBehindCount({
repoPath,
defaultBranch,
}: {
repoPath: string;
defaultBranch: string;
}): Promise<{ ahead: number; behind: number }> {
const git = simpleGit(repoPath);
try {
const output = await git.raw([
"rev-list",
"--left-right",
"--count",
`origin/${defaultBranch}...HEAD`,
]);
const [behindStr, aheadStr] = output.trim().split(/\s+/);
return {
ahead: Number.parseInt(aheadStr || "0", 10),
behind: Number.parseInt(behindStr || "0", 10),
};
} catch {
return { ahead: 0, behind: 0 };
}
}
Comment on lines +1093 to +1116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for all references to checkNeedsRebase in the codebase
rg -n 'checkNeedsRebase' --type=ts --type=tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Check the git.ts file for `any` type usage and other potential issues
head -100 apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts

Repository: superset-sh/superset

Length of output: 2626


🏁 Script executed:

# Check if the file uses proper aliases as defined in tsconfig
grep -n "import.*from" apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts | head -20

Repository: superset-sh/superset

Length of output: 612


🏁 Script executed:

# Search for all references to checkNeedsRebase without file type restriction
rg -n 'checkNeedsRebase' apps/desktop/src/lib/trpc/routers/workspaces/utils/

Repository: superset-sh/superset

Length of output: 166


🏁 Script executed:

# Read lines 1070-1120 to see checkNeedsRebase and the new function in context
sed -n '1070,1120p' apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts

Repository: superset-sh/superset

Length of output: 1144


🏁 Script executed:

# Search for any type usage in git.ts
rg -n ': any' apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check if checkNeedsRebase is exported from git.ts and imported elsewhere
rg -n 'export.*checkNeedsRebase|import.*checkNeedsRebase' apps/desktop/

Repository: superset-sh/superset

Length of output: 166


🏁 Script executed:

# Search entire repository for checkNeedsRebase imports/usage (not just definition)
rg -n 'checkNeedsRebase' --type ts

Repository: superset-sh/superset

Length of output: 166


🏁 Script executed:

# Specifically check git-status.ts mentioned in the review
find apps/desktop -name "git-status.ts" -o -name "*git-status*" | head -5

Repository: superset-sh/superset

Length of output: 133


🏁 Script executed:

# Look for any references to checkNeedsRebase in the whole repo
rg 'checkNeedsRebase' -g '!git.ts'

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check git-status.ts to see how getAheadBehindCount is used
cat -n apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts | head -50

Repository: superset-sh/superset

Length of output: 1817


🏁 Script executed:

# Also search git-status.ts for any references to checkNeedsRebase or needsRebase
rg -n 'needsRebase|checkNeedsRebase' apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts

Repository: superset-sh/superset

Length of output: 96


Remove the checkNeedsRebase function as dead code.

The getAheadBehindCount function correctly parses --left-right --count output, mapping the first column (left/remote) to behind and the second column (right/HEAD) to ahead. Silent error fallback to {0, 0} is reasonable for a non-critical status indicator.

The existing checkNeedsRebase function (line 1080) is no longer used—git-status.ts now derives needsRebase from behind > 0 using the new getAheadBehindCount. Remove this function to avoid confusion.

🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts` around lines 1093
- 1116, Remove the dead function checkNeedsRebase from the file (it’s no longer
referenced now that getAheadBehindCount drives needsRebase via behind > 0);
locate the checkNeedsRebase declaration and delete its entire definition and any
associated unused imports or helper constants only used by it so there’s no
leftover unused symbols (leave getAheadBehindCount intact).


export async function hasUncommittedChanges(
worktreePath: string,
): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export async function initializeWorkspaceWorktree({
gitStatus: {
branch,
needsRebase: false,
ahead: 0,
behind: 0,
lastRefreshed: Date.now(),
},
})
Expand Down Expand Up @@ -438,6 +440,8 @@ export async function initializeWorkspaceWorktree({
gitStatus: {
branch,
needsRebase: false,
ahead: 0,
behind: 0,
lastRefreshed: Date.now(),
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "@superset/ui/context-menu";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@superset/ui/hover-card";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { cn } from "@superset/ui/utils";
import type { RefObject } from "react";
import { LuCopy, LuX } from "react-icons/lu";
import type { ActivePaneStatus } from "shared/tabs-types";
import { STROKE_WIDTH } from "../constants";
import { DeleteWorkspaceDialog, WorkspaceHoverCardContent } from "./components";
import { HOVER_CARD_CLOSE_DELAY, HOVER_CARD_OPEN_DELAY } from "./constants";
import { WorkspaceIcon } from "./WorkspaceIcon";

interface CollapsedWorkspaceItemProps {
id: string;
name: string;
branch: string;
type: "worktree" | "branch";
isActive: boolean;
isUnread: boolean;
workspaceStatus: ActivePaneStatus | null;
itemRef: RefObject<HTMLElement | null>;
showDeleteDialog: boolean;
setShowDeleteDialog: (open: boolean) => void;
onMouseEnter: () => void;
onClick: () => void;
onDeleteClick: () => void;
onCopyPath: () => void;
}

export function CollapsedWorkspaceItem({
id,
name,
branch,
type,
isActive,
isUnread,
workspaceStatus,
itemRef,
showDeleteDialog,
setShowDeleteDialog,
onMouseEnter,
onClick,
onDeleteClick,
onCopyPath,
}: CollapsedWorkspaceItemProps) {
const isBranchWorkspace = type === "branch";

const collapsedButton = (
<button
ref={(node) => {
(itemRef as React.MutableRefObject<HTMLElement | null>).current = node;
}}
type="button"
onClick={onClick}
onMouseEnter={onMouseEnter}
className={cn(
"relative flex items-center justify-center size-8 rounded-md",
"hover:bg-muted/50 transition-colors",
isActive && "bg-muted",
)}
>
<WorkspaceIcon
isBranchWorkspace={isBranchWorkspace}
isActive={isActive}
isUnread={isUnread}
workspaceStatus={workspaceStatus}
variant="collapsed"
/>
</button>
);

if (isBranchWorkspace) {
return (
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>{collapsedButton}</TooltipTrigger>
<TooltipContent side="right" className="flex flex-col gap-0.5">
<span className="font-medium">local</span>
<span className="text-xs text-muted-foreground font-mono">
{branch}
</span>
</TooltipContent>
</Tooltip>
);
}

return (
<>
<HoverCard
openDelay={HOVER_CARD_OPEN_DELAY}
closeDelay={HOVER_CARD_CLOSE_DELAY}
>
<ContextMenu>
<HoverCardTrigger asChild>
<ContextMenuTrigger asChild>{collapsedButton}</ContextMenuTrigger>
</HoverCardTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={onCopyPath}>
<LuCopy className="size-4 mr-2" strokeWidth={STROKE_WIDTH} />
Copy Path
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onSelect={() => onDeleteClick()}>
<LuX className="size-4 mr-2" strokeWidth={STROKE_WIDTH} />
Close Worktree
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<HoverCardContent side="right" align="start" className="w-72">
<WorkspaceHoverCardContent workspaceId={id} workspaceAlias={name} />
</HoverCardContent>
</HoverCard>
<DeleteWorkspaceDialog
workspaceId={id}
workspaceName={name}
workspaceType={type}
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
interface WorkspaceAheadBehindProps {
ahead: number;
behind: number;
}

export function WorkspaceAheadBehind({
ahead,
behind,
}: WorkspaceAheadBehindProps) {
if (ahead === 0 && behind === 0) {
return null;
}

return (
<div className="flex items-center gap-1.5 text-[10px] font-mono tabular-nums shrink-0">
{behind > 0 && <span className="text-amber-500">↓{behind}</span>}
{ahead > 0 && <span className="text-emerald-500">↑{ahead}</span>}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,15 @@ export function WorkspaceDiffStats({
: "bg-muted/50 group-hover:bg-transparent",
)}
>
{/* Diff stats - hidden on card hover when onClose provided */}
<div
className={
onClose
? "flex items-center gap-1.5 group-hover:hidden"
: "flex items-center gap-1.5"
}
className={cn(
"flex items-center gap-1.5",
onClose && "group-hover:hidden",
)}
>
<span className="text-emerald-500/90">+{additions}</span>
<span className="text-red-400/90">−{deletions}</span>
</div>
{/* X icon - shown on card hover */}
{onClose && (
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
Expand Down
Loading