Skip to content
Draft
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 @@ -18,16 +18,15 @@ import { HotkeyLabel, useHotkey, useHotkeyDisplay } from "renderer/hotkeys";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useV2ProjectDefaultApp } from "renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp";
import { useThemeStore } from "renderer/stores";
import { getV2WorktreeDisplayName } from "./utils/getV2WorktreeDisplayName";

interface V2OpenInMenuButtonProps {
worktreePath: string;
branch: string;
projectId: string;
}

export function V2OpenInMenuButton({
worktreePath,
branch,
projectId,
}: V2OpenInMenuButtonProps) {
const activeTheme = useThemeStore((state) => state.activeTheme);
Expand Down Expand Up @@ -57,6 +56,10 @@ export function V2OpenInMenuButton({
const showCopyPathShortcut = copyPathDisplay.text !== "Unassigned";
const isLoading = openInApp.isPending || copyPath.isPending;
const isDark = activeTheme?.type === "dark";
const displayName = useMemo(
() => getV2WorktreeDisplayName(worktreePath, projectId),
[worktreePath, projectId],
);

const handleOpenInEditor = useCallback(() => {
if (openInApp.isPending || copyPath.isPending) return;
Expand Down Expand Up @@ -107,9 +110,9 @@ export function V2OpenInMenuButton({
className="size-3.5 object-contain shrink-0"
/>
)}
{branch && (
{displayName && (
<span className="hidden lg:inline text-muted-foreground truncate max-w-[140px] tabular-nums">
/{branch}
/{displayName}
</span>
)}
<span className="hidden sm:inline text-foreground font-medium">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, test } from "bun:test";
import { getV2WorktreeDisplayName } from "./getV2WorktreeDisplayName";

describe("getV2WorktreeDisplayName", () => {
const projectId = "proj-abc";

test("returns the branch-at-creation directory name from a v2 worktree path", () => {
const path = `/home/user/.superset/worktrees/${projectId}/andrew/foo`;
expect(getV2WorktreeDisplayName(path, projectId)).toBe("andrew/foo");
});

test("preserves nested slashed branch names", () => {
const path = `/home/user/.superset/worktrees/${projectId}/feat/users/list`;
expect(getV2WorktreeDisplayName(path, projectId)).toBe("feat/users/list");
});

test("does NOT change with the workspace's current branch — the fix scenario from #3759", () => {
// Workspace was created from `andrew/foo`, so the directory is fixed there.
// After stacked PR navigation, `workspaces.branch` becomes `andrew/foo-2`.
// The displayed label must reflect the on-disk directory, not the live branch.
const persistedPath = `/home/user/.superset/worktrees/${projectId}/andrew/foo`;
const liveBranchAfterStackNavigation = "andrew/foo-2";

const display = getV2WorktreeDisplayName(persistedPath, projectId);

expect(display).toBe("andrew/foo");
expect(display).not.toBe(liveBranchAfterStackNavigation);
});

test("falls back to the basename if the projectId marker is absent", () => {
const path = "/some/other/location/my-branch";
expect(getV2WorktreeDisplayName(path, projectId)).toBe("my-branch");
});

test("handles Windows-style separators in the fallback", () => {
const path = "C:\\Users\\me\\some\\place\\my-branch";
expect(getV2WorktreeDisplayName(path, projectId)).toBe("my-branch");
});

test("returns the original string if no separator is present", () => {
expect(getV2WorktreeDisplayName("standalone", projectId)).toBe(
"standalone",
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// V2 worktrees live at `<homedir>/.superset/worktrees/<projectId>/<branchAtCreation>`.
// The directory name is fixed at create time; `workspaces.branch` drifts as HEAD
// moves (git status sync, AI rename, manual checkout). Derive the label from the
// persisted path so it always matches the on-disk directory.
export function getV2WorktreeDisplayName(
worktreePath: string,
projectId: string,
): string {
const marker = `worktrees/${projectId}/`;
const idx = worktreePath.indexOf(marker);
if (idx >= 0) {
const tail = worktreePath.slice(idx + marker.length);
if (tail.length > 0) return tail;
}
const lastSep = Math.max(
worktreePath.lastIndexOf("/"),
worktreePath.lastIndexOf("\\"),
);
return lastSep >= 0 ? worktreePath.slice(lastSep + 1) : worktreePath;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getV2WorktreeDisplayName } from "./getV2WorktreeDisplayName";
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export function V2WorkspaceOpenInButton({
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
.select(({ workspaces, hosts }) => ({
id: workspaces.id,
branch: workspaces.branch,
projectId: workspaces.projectId,
hostMachineId: hosts?.machineId ?? null,
})),
Expand Down Expand Up @@ -55,7 +54,6 @@ export function V2WorkspaceOpenInButton({

return (
<V2OpenInMenuButton
branch={workspace.branch}
worktreePath={workspaceQuery.data.worktreePath}
projectId={workspace.projectId}
/>
Expand Down