优化远程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) Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout)
.lines() .lines()
.map(|l| l.trim().to_string()) .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(), .collect(),
_ => vec![], _ => vec![],
}; };

View File

@@ -1315,6 +1315,19 @@ export function FileTreeTab() {
} }
}, [compareBranchList, compareFilterKeyword]) }, [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(() => { const directoryGitTreeNodes = useMemo(() => {
if (!directoryGitActionTarget) return [] if (!directoryGitActionTarget) return []
return buildDirectoryGitTree( return buildDirectoryGitTree(
@@ -2455,21 +2468,58 @@ export function FileTreeTab() {
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1"> <CollapsibleContent className="space-y-1 pt-1">
{filteredCompareBranches.remote.length > 0 ? ( {filteredCompareBranches.remote.length > 0 ? (
filteredCompareBranches.remote.map((branch) => ( hasMultipleCompareRemotes ? (
<Button compareRemoteNames.map((remoteName) => (
key={`remote-${branch}`} <Collapsible key={remoteName}>
type="button" <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">
size="xs" <ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
variant="ghost" {remoteName} ({groupedCompareRemoteBranches[remoteName].length})
className="w-full justify-start" </CollapsibleTrigger>
onClick={() => { <CollapsibleContent className="space-y-1 pt-1 pl-3">
void handleCompareBranchClick(branch) {groupedCompareRemoteBranches[remoteName].map(
}} (branch) => (
disabled={comparing} <Button
> key={`remote-${branch}`}
{branch} type="button"
</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"> <div className="px-2 text-xs text-muted-foreground">
{t("compareDialog.noMatchingBranches")} {t("compareDialog.noMatchingBranches")}

View File

@@ -10,6 +10,8 @@ import {
} from "react" } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { import {
ChevronDown,
ChevronRight,
ChevronsDownUp, ChevronsDownUp,
ChevronsUpDown, ChevronsUpDown,
CircleHelp, CircleHelp,
@@ -62,15 +64,15 @@ import {
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { import {
Select, Popover,
SelectContent, PopoverContent,
SelectGroup, PopoverTrigger,
SelectItem, } from "@/components/ui/popover"
SelectLabel, import {
SelectSeparator, Collapsible,
SelectTrigger, CollapsibleContent,
SelectValue, CollapsibleTrigger,
} from "@/components/ui/select" } from "@/components/ui/collapsible"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
import { useWorkspaceContext } from "@/contexts/workspace-context" import { useWorkspaceContext } from "@/contexts/workspace-context"
@@ -527,55 +529,126 @@ function BranchSelector({
refreshing: boolean refreshing: boolean
}) { }) {
const t = useTranslations("Folder.gitLogTab.branchSelector") 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 ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Select value={selectedBranch ?? ""} onValueChange={onBranchChange}> <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<SelectTrigger <PopoverTrigger asChild>
size="sm" <Button
className="cursor-pointer flex-1 w-full text-xs bg-input/30 hover:bg-input/50 aria-expanded:bg-muted" 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" /> <div className="max-h-72 overflow-y-auto">
<SelectValue placeholder={t("selectBranchPlaceholder")} /> {branchList.local.length > 0 && (
</SelectTrigger> <Collapsible open={localOpen} onOpenChange={setLocalOpen}>
<SelectContent position="popper" sideOffset={4}> <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">
{branchList.local.length > 0 && ( <ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
<SelectGroup> {t("localBranches")}
<SelectLabel>{t("localBranches")}</SelectLabel> </CollapsibleTrigger>
{branchList.local.map((branch) => ( <CollapsibleContent>
<SelectItem {branchList.local.map((branch) =>
key={`local-${branch}`} renderBranchItem(branch, undefined, 1)
value={branch}
className="text-xs"
>
{branch}
{branch === currentBranch && (
<span className="ml-auto text-[10px] text-muted-foreground">
{t("current")}
</span>
)} )}
</SelectItem> </CollapsibleContent>
))} </Collapsible>
</SelectGroup> )}
)} {branchList.remote.length > 0 && (
{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">
{branchList.local.length > 0 && <SelectSeparator />} <ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
<SelectGroup> {t("remoteBranches")}
<SelectLabel>{t("remoteBranches")}</SelectLabel> </CollapsibleTrigger>
{branchList.remote.map((branch) => ( <CollapsibleContent>
<SelectItem {hasMultipleRemotes ? (
key={`remote-${branch}`} remoteNames.map((remoteName) => (
value={branch} <Collapsible key={remoteName}>
className="text-xs" <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" />
{branch} {remoteName} ({groupedRemoteBranches[remoteName].length})
</SelectItem> </CollapsibleTrigger>
))} <CollapsibleContent>
</SelectGroup> {groupedRemoteBranches[remoteName].map((branch) =>
</> renderBranchItem(
)} branch,
</SelectContent> branch.substring(remoteName.length + 1),
</Select> 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 <Button
variant="outline" variant="outline"
size="icon" 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 isCurrent = b === branch
const isWorktree = worktreeBranchSet.has( const isWorktree = worktreeBranchSet.has(
isRemote ? b.replace(/^[^/]+\//, "") : b 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" 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" /> <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> <span className="ml-auto text-xs">{t("current")}</span>
</div> </div>
) )
@@ -412,7 +417,7 @@ export function BranchDropdown({
onPointerLeave={(e) => e.preventDefault()} onPointerLeave={(e) => e.preventDefault()}
> >
<BranchIcon className="h-3.5 w-3.5" /> <BranchIcon className="h-3.5 w-3.5" />
{b} {label}
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
<DropdownMenuSubContent> <DropdownMenuSubContent>
<DropdownMenuItem <DropdownMenuItem
@@ -678,15 +683,24 @@ export function BranchDropdown({
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" /> <ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{remoteName} ({groupedRemoteBranches[remoteName].length}) {remoteName} ({groupedRemoteBranches[remoteName].length})
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent className="pl-3">
{groupedRemoteBranches[remoteName].map((b) => {groupedRemoteBranches[remoteName].map((b) =>
renderBranchItem(b, true) renderBranchItem(
b,
true,
b.substring(remoteName.length + 1)
)
)} )}
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </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> </CollapsibleContent>
</Collapsible> </Collapsible>