优化远程Git分组显示

This commit is contained in:
xintaofei
2026-03-13 23:20:25 +08:00
parent 767d43b0cf
commit 874591a473
4 changed files with 213 additions and 76 deletions

View File

@@ -1066,7 +1066,7 @@ pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCom
Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout)
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.contains("HEAD"))
.filter(|l| !l.is_empty() && !l.contains("HEAD") && l.contains('/'))
.collect(),
_ => vec![],
};

View File

@@ -1315,6 +1315,19 @@ export function FileTreeTab() {
}
}, [compareBranchList, compareFilterKeyword])
const groupedCompareRemoteBranches = useMemo(() => {
const groups: Record<string, string[]> = {}
for (const b of filteredCompareBranches.remote) {
const slashIndex = b.indexOf("/")
const remoteName = slashIndex > 0 ? b.substring(0, slashIndex) : "origin"
if (!groups[remoteName]) groups[remoteName] = []
groups[remoteName].push(b)
}
return groups
}, [filteredCompareBranches.remote])
const compareRemoteNames = Object.keys(groupedCompareRemoteBranches)
const hasMultipleCompareRemotes = compareRemoteNames.length > 1
const directoryGitTreeNodes = useMemo(() => {
if (!directoryGitActionTarget) return []
return buildDirectoryGitTree(
@@ -2455,21 +2468,58 @@ export function FileTreeTab() {
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1">
{filteredCompareBranches.remote.length > 0 ? (
filteredCompareBranches.remote.map((branch) => (
<Button
key={`remote-${branch}`}
type="button"
size="xs"
variant="ghost"
className="w-full justify-start"
onClick={() => {
void handleCompareBranchClick(branch)
}}
disabled={comparing}
>
{branch}
</Button>
))
hasMultipleCompareRemotes ? (
compareRemoteNames.map((remoteName) => (
<Collapsible key={remoteName}>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-2 py-1.5 pl-5 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{remoteName} ({groupedCompareRemoteBranches[remoteName].length})
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1 pl-3">
{groupedCompareRemoteBranches[remoteName].map(
(branch) => (
<Button
key={`remote-${branch}`}
type="button"
size="xs"
variant="ghost"
className="w-full justify-start"
onClick={() => {
void handleCompareBranchClick(branch)
}}
disabled={comparing}
>
{branch.substring(remoteName.length + 1)}
</Button>
)
)}
</CollapsibleContent>
</Collapsible>
))
) : (
filteredCompareBranches.remote.map((branch) => {
const slashIndex = branch.indexOf("/")
const shortName =
slashIndex > 0
? branch.substring(slashIndex + 1)
: branch
return (
<Button
key={`remote-${branch}`}
type="button"
size="xs"
variant="ghost"
className="w-full justify-start pl-4"
onClick={() => {
void handleCompareBranchClick(branch)
}}
disabled={comparing}
>
{shortName}
</Button>
)
})
)
) : (
<div className="px-2 text-xs text-muted-foreground">
{t("compareDialog.noMatchingBranches")}

View File

@@ -10,6 +10,8 @@ import {
} from "react"
import { useTranslations } from "next-intl"
import {
ChevronDown,
ChevronRight,
ChevronsDownUp,
ChevronsUpDown,
CircleHelp,
@@ -62,15 +64,15 @@ import {
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { Skeleton } from "@/components/ui/skeleton"
import { useFolderContext } from "@/contexts/folder-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
@@ -527,55 +529,126 @@ function BranchSelector({
refreshing: boolean
}) {
const t = useTranslations("Folder.gitLogTab.branchSelector")
const [popoverOpen, setPopoverOpen] = useState(false)
const [localOpen, setLocalOpen] = useState(true)
const [remoteOpen, setRemoteOpen] = useState(false)
const groupedRemoteBranches = useMemo(() => {
const groups: Record<string, string[]> = {}
for (const b of branchList.remote) {
const slashIndex = b.indexOf("/")
const remoteName = slashIndex > 0 ? b.substring(0, slashIndex) : "origin"
if (!groups[remoteName]) groups[remoteName] = []
groups[remoteName].push(b)
}
return groups
}, [branchList.remote])
const remoteNames = Object.keys(groupedRemoteBranches)
const hasMultipleRemotes = remoteNames.length > 1
const handleSelect = (branch: string) => {
onBranchChange(branch)
setPopoverOpen(false)
}
function renderBranchItem(
branch: string,
displayName?: string,
indent = 0
) {
const isCurrent = branch === selectedBranch
return (
<button
key={branch}
type="button"
className={`flex w-full items-center gap-2 rounded-lg py-1.5 text-xs hover:bg-accent hover:text-accent-foreground select-none outline-hidden ${isCurrent ? "bg-accent/50" : ""}`}
style={{ paddingLeft: `${(indent + 1) * 0.5 + 0.5}rem` }}
onClick={() => handleSelect(branch)}
>
<span className="truncate">{displayName ?? branch}</span>
{branch === currentBranch && (
<span className="ml-auto pr-2 text-[10px] text-muted-foreground">
{t("current")}
</span>
)}
</button>
)
}
return (
<div className="flex items-center gap-1">
<Select value={selectedBranch ?? ""} onValueChange={onBranchChange}>
<SelectTrigger
size="sm"
className="cursor-pointer flex-1 w-full text-xs bg-input/30 hover:bg-input/50 aria-expanded:bg-muted"
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="cursor-pointer flex-1 w-full text-xs bg-input/30 hover:bg-input/50 justify-start gap-1.5"
>
<GitBranch className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">
{selectedBranch || t("selectBranchPlaceholder")}
</span>
<ChevronDown className="ml-auto h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-64 p-1"
side="bottom"
align="start"
sideOffset={4}
>
<GitBranch className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<SelectValue placeholder={t("selectBranchPlaceholder")} />
</SelectTrigger>
<SelectContent position="popper" sideOffset={4}>
{branchList.local.length > 0 && (
<SelectGroup>
<SelectLabel>{t("localBranches")}</SelectLabel>
{branchList.local.map((branch) => (
<SelectItem
key={`local-${branch}`}
value={branch}
className="text-xs"
>
{branch}
{branch === currentBranch && (
<span className="ml-auto text-[10px] text-muted-foreground">
{t("current")}
</span>
<div className="max-h-72 overflow-y-auto">
{branchList.local.length > 0 && (
<Collapsible open={localOpen} onOpenChange={setLocalOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{t("localBranches")}
</CollapsibleTrigger>
<CollapsibleContent>
{branchList.local.map((branch) =>
renderBranchItem(branch, undefined, 1)
)}
</SelectItem>
))}
</SelectGroup>
)}
{branchList.remote.length > 0 && (
<>
{branchList.local.length > 0 && <SelectSeparator />}
<SelectGroup>
<SelectLabel>{t("remoteBranches")}</SelectLabel>
{branchList.remote.map((branch) => (
<SelectItem
key={`remote-${branch}`}
value={branch}
className="text-xs"
>
{branch}
</SelectItem>
))}
</SelectGroup>
</>
)}
</SelectContent>
</Select>
</CollapsibleContent>
</Collapsible>
)}
{branchList.remote.length > 0 && (
<Collapsible open={remoteOpen} onOpenChange={setRemoteOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{t("remoteBranches")}
</CollapsibleTrigger>
<CollapsibleContent>
{hasMultipleRemotes ? (
remoteNames.map((remoteName) => (
<Collapsible key={remoteName}>
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded-lg py-1.5 pl-5 text-xs hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{remoteName} ({groupedRemoteBranches[remoteName].length})
</CollapsibleTrigger>
<CollapsibleContent>
{groupedRemoteBranches[remoteName].map((branch) =>
renderBranchItem(
branch,
branch.substring(remoteName.length + 1),
3
)
)}
</CollapsibleContent>
</Collapsible>
))
) : (
branchList.remote.map((branch) => {
const slashIndex = branch.indexOf("/")
const shortName =
slashIndex > 0 ? branch.substring(slashIndex + 1) : branch
return renderBranchItem(branch, shortName, 1)
})
)}
</CollapsibleContent>
</Collapsible>
)}
</div>
</PopoverContent>
</Popover>
<Button
variant="outline"
size="icon"

View File

@@ -368,7 +368,12 @@ export function BranchDropdown({
}
}
function renderBranchItem(b: string, isRemote: boolean) {
function renderBranchItem(
b: string,
isRemote: boolean,
displayName?: string
) {
const label = displayName ?? b
const isCurrent = b === branch
const isWorktree = worktreeBranchSet.has(
isRemote ? b.replace(/^[^/]+\//, "") : b
@@ -382,7 +387,7 @@ export function BranchDropdown({
className="flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm opacity-50 select-none"
>
<BranchIcon className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{b}</span>
<span className="truncate">{label}</span>
<span className="ml-auto text-xs">{t("current")}</span>
</div>
)
@@ -412,7 +417,7 @@ export function BranchDropdown({
onPointerLeave={(e) => e.preventDefault()}
>
<BranchIcon className="h-3.5 w-3.5" />
{b}
{label}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem
@@ -678,15 +683,24 @@ export function BranchDropdown({
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{remoteName} ({groupedRemoteBranches[remoteName].length})
</CollapsibleTrigger>
<CollapsibleContent>
<CollapsibleContent className="pl-3">
{groupedRemoteBranches[remoteName].map((b) =>
renderBranchItem(b, true)
renderBranchItem(
b,
true,
b.substring(remoteName.length + 1)
)
)}
</CollapsibleContent>
</Collapsible>
))
) : (
branchList.remote.map((b) => renderBranchItem(b, true))
branchList.remote.map((b) => {
const slashIndex = b.indexOf("/")
const shortName =
slashIndex > 0 ? b.substring(slashIndex + 1) : b
return renderBranchItem(b, true, shortName)
})
)}
</CollapsibleContent>
</Collapsible>