优化远程Git分组显示
This commit is contained in:
@@ -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![],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user