添加git推送窗口,显示待提交列表和查看文件差异

This commit is contained in:
xintaofei
2026-03-21 20:37:19 +08:00
parent 048b8a8480
commit d9032f1c82
18 changed files with 986 additions and 137 deletions

View File

@@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "settings"], "windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "push-*", "settings"],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:default", "core:window:default",

View File

@@ -597,3 +597,42 @@ pub async fn open_stash_window(
Ok(()) Ok(())
} }
#[tauri::command]
pub async fn open_push_window(
app: AppHandle,
db: tauri::State<'_, AppDatabase>,
folder_id: i32,
) -> Result<(), AppCommandError> {
let label = format!("push-{folder_id}");
if let Some(existing) = app.get_webview_window(&label) {
ensure_windows_undecorated(&existing);
let _ = existing.unminimize();
existing
.set_focus()
.map_err(|e| AppCommandError::window("Failed to focus push window", e.to_string()))?;
return Ok(());
}
let folder = crate::db::service::folder_service::get_folder_by_id(&db.conn, folder_id)
.await
.map_err(AppCommandError::from)?
.ok_or_else(|| {
AppCommandError::not_found(format!("Folder {folder_id} not found"))
.with_detail(format!("folder_id={folder_id}"))
})?;
let url = WebviewUrl::App(format!("push?folderId={folder_id}").into());
let builder = WebviewWindowBuilder::new(&app, &label, url)
.title(format!("Push - {}", folder.name))
.inner_size(1100.0, 700.0)
.min_inner_size(800.0, 500.0)
.center();
let push_window = apply_platform_window_style(builder)
.build()
.map_err(|e| AppCommandError::window("Failed to open push window", e.to_string()))?;
ensure_windows_undecorated(&push_window);
Ok(())
}

View File

@@ -263,6 +263,7 @@ pub fn run() {
windows::focus_folder_window, windows::focus_folder_window,
windows::open_merge_window, windows::open_merge_window,
windows::open_stash_window, windows::open_stash_window,
windows::open_push_window,
system_settings::get_system_proxy_settings, system_settings::get_system_proxy_settings,
system_settings::update_system_proxy_settings, system_settings::update_system_proxy_settings,
system_settings::get_system_language_settings, system_settings::get_system_language_settings,

8
src/app/push/layout.tsx Normal file
View File

@@ -0,0 +1,8 @@
"use client"
import type { ReactNode } from "react"
import { GitCredentialProvider } from "@/contexts/git-credential-context"
export default function PushLayout({ children }: { children: ReactNode }) {
return <GitCredentialProvider>{children}</GitCredentialProvider>
}

111
src/app/push/page.tsx Normal file
View File

@@ -0,0 +1,111 @@
"use client"
import { Suspense, useEffect, useState } from "react"
import { useSearchParams } from "next/navigation"
import { useTranslations } from "next-intl"
import { Loader2 } from "lucide-react"
import { PushWorkspace } from "@/components/layout/push-workspace"
import { AppTitleBar } from "@/components/layout/app-title-bar"
import { AppToaster } from "@/components/ui/app-toaster"
import { getFolder } from "@/lib/tauri"
import type { FolderDetail } from "@/lib/types"
const TOAST_DURATION_MS = 6000
interface FolderLoadState {
loadedId: number | null
folder: FolderDetail | null
error: string | null
}
function PushPageInner() {
const t = useTranslations("Folder.pushWindow")
const searchParams = useSearchParams()
const [state, setState] = useState<FolderLoadState>({
loadedId: null,
folder: null,
error: null,
})
const folderId = Number(searchParams.get("folderId") ?? "0")
const normalizedFolderId = Number.isFinite(folderId) ? folderId : 0
const hasValidFolderId = normalizedFolderId > 0
const loading = hasValidFolderId && state.loadedId !== normalizedFolderId
const folder = state.loadedId === normalizedFolderId ? state.folder : null
const error = state.loadedId === normalizedFolderId ? state.error : null
useEffect(() => {
if (!hasValidFolderId) return
let cancelled = false
getFolder(normalizedFolderId)
.then((detail) => {
if (!cancelled) {
setState({
loadedId: normalizedFolderId,
folder: detail,
error: null,
})
}
})
.catch((err) => {
if (!cancelled) {
setState({
loadedId: normalizedFolderId,
folder: null,
error: String(err),
})
}
})
return () => {
cancelled = true
}
}, [hasValidFolderId, normalizedFolderId])
return (
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
<AppTitleBar
center={
<div className="text-sm font-semibold tracking-tight">
{t("title")}
{hasValidFolderId && folder ? ` · ${folder.name}` : ""}
</div>
}
/>
<main className="min-h-0 flex-1">
{!hasValidFolderId ? (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
Invalid folder ID
</div>
) : loading ? (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
</div>
) : error ? (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
) : folder ? (
<PushWorkspace folderPath={folder.path} folderName={folder.name} />
) : null}
</main>
<AppToaster
position="bottom-right"
duration={TOAST_DURATION_MS}
closeButton
/>
</div>
)
}
export default function PushPage() {
return (
<Suspense>
<PushPageInner />
</Suspense>
)
}

View File

@@ -67,7 +67,6 @@ import {
gitInit, gitInit,
gitPull, gitPull,
gitFetch, gitFetch,
gitPush,
gitNewBranch, gitNewBranch,
gitWorktreeAdd, gitWorktreeAdd,
gitCheckout, gitCheckout,
@@ -78,9 +77,8 @@ import {
openFolderWindow, openFolderWindow,
openCommitWindow, openCommitWindow,
setFolderParentBranch, setFolderParentBranch,
gitListConflicts,
gitHasMergeHead,
openStashWindow, openStashWindow,
openPushWindow,
} from "@/lib/tauri" } from "@/lib/tauri"
import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog" import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog"
import { ConflictDialog } from "@/components/layout/conflict-dialog" import { ConflictDialog } from "@/components/layout/conflict-dialog"
@@ -300,138 +298,6 @@ export function BranchDropdown({
}) })
} }
// Uses operation "merge" intentionally: MERGE_HEAD exists so merge state is
// already active. MergeWorkspace won't call gitStartPullMerge (only for "pull"),
// and ConflictDialog abort correctly runs git merge --abort.
async function showMergeConflictDialog() {
try {
const remaining = await gitListConflicts(folderPath)
setConflictInfo({
has_conflicts: true,
conflicted_files: remaining,
operation: "merge",
})
} catch {
setConflictInfo({
has_conflicts: true,
conflicted_files: [],
operation: "merge",
})
}
}
async function handlePush() {
// Pre-check: if MERGE_HEAD exists, show conflict dialog immediately
try {
if (await gitHasMergeHead(folderPath)) {
await showMergeConflictDialog()
return
}
} catch {
// Pre-check failed, continue with normal push flow
}
const taskId = `git-${++taskSeq.current}-${Date.now()}`
const label = t("tasks.pushCode")
setLoading(true)
addTask(taskId, label)
updateTask(taskId, { status: "running" })
try {
const result = await withCredentialRetry(
(creds) => gitPush(folderPath, creds),
{ folderPath }
)
updateTask(taskId, { status: "completed" })
onBranchChange()
let description: string | undefined
if (result.upstream_set) {
description =
result.pushed_commits === 0
? t("toasts.upstreamSet")
: t("toasts.upstreamSetAndPushed", {
count: result.pushed_commits,
})
} else if (result.pushed_commits === 0) {
description = t("toasts.noCommitsToPush")
} else {
description = t("toasts.pushedCommits", {
count: result.pushed_commits,
})
}
toast.success(t("toasts.taskCompleted", { label }), {
description,
})
} catch (err) {
const errorMsg = toErrorMessage(err)
if (/MERGE_HEAD|unfinished merge/i.test(errorMsg)) {
// Unfinished merge — show conflict dialog
removeTask(taskId)
await showMergeConflictDialog()
} else if (/rejected|fetch first/i.test(errorMsg)) {
// Remote has new commits — auto-pull then retry push
updateTask(taskId, {
status: "running",
})
try {
const pullResult = await withCredentialRetry(
(creds) => gitPull(folderPath, creds),
{ folderPath }
)
if (pullResult.conflict?.has_conflicts) {
removeTask(taskId)
onBranchChange()
setConflictInfo(pullResult.conflict)
} else {
// Pull succeeded, retry push
updateTask(taskId, { status: "running" })
const pushResult = await withCredentialRetry(
(creds) => gitPush(folderPath, creds),
{ folderPath }
)
updateTask(taskId, { status: "completed" })
onBranchChange()
let description: string | undefined
if (pushResult.upstream_set) {
description =
pushResult.pushed_commits === 0
? t("toasts.upstreamSet")
: t("toasts.upstreamSetAndPushed", {
count: pushResult.pushed_commits,
})
} else if (pushResult.pushed_commits === 0) {
description = t("toasts.noCommitsToPush")
} else {
description = t("toasts.pushedCommits", {
count: pushResult.pushed_commits,
})
}
toast.success(t("toasts.taskCompleted", { label }), {
description,
})
}
} catch (pullErr) {
const pullErrMsg = toErrorMessage(pullErr)
if (/MERGE_HEAD|unfinished merge/i.test(pullErrMsg)) {
removeTask(taskId)
await showMergeConflictDialog()
} else {
removeTask(taskId)
const pullErrTitle = t("toasts.taskFailed", { label })
pushAlert("error", pullErrTitle, pullErrMsg)
toast.error(pullErrTitle, { description: pullErrMsg })
}
}
} else {
removeTask(taskId)
const errorTitle = t("toasts.taskFailed", { label })
pushAlert("error", errorTitle, errorMsg)
toast.error(errorTitle, { description: errorMsg })
}
} finally {
setLoading(false)
}
}
function handleMergeParent() { function handleMergeParent() {
if (!parentBranch) return if (!parentBranch) return
setConfirmAction({ type: "merge", branchName: parentBranch }) setConfirmAction({ type: "merge", branchName: parentBranch })
@@ -751,7 +617,19 @@ export function BranchDropdown({
<GitCommitHorizontal className="h-3.5 w-3.5" /> <GitCommitHorizontal className="h-3.5 w-3.5" />
{t("openCommitWindow")} {t("openCommitWindow")}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem disabled={loading} onSelect={handlePush}> <DropdownMenuItem
disabled={loading}
onSelect={() => {
if (!folder) return
setDropdownOpen(false)
openPushWindow(folder.id).catch((err) => {
const title = t("toasts.openPushWindowFailed")
const msg = toErrorMessage(err)
pushAlert("error", title, msg)
toast.error(title, { description: msg })
})
}}
>
<Upload className="h-3.5 w-3.5" /> <Upload className="h-3.5 w-3.5" />
{t("pushCode")} {t("pushCode")}
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -0,0 +1,628 @@
"use client"
import type { ReactElement } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import {
ChevronsDownUp,
ChevronsUpDown,
CloudOff,
Loader2,
Upload,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import {
FileTree,
FileTreeFile,
FileTreeFolder,
} from "@/components/ai-elements/file-tree"
import {
Commit,
CommitContent,
CommitFileAdditions,
CommitFileChanges,
CommitFileDeletions,
CommitFileIcon,
CommitFileInfo,
CommitFilePath,
CommitFiles,
CommitFileStatus,
CommitHash,
CommitHeader,
CommitInfo,
CommitMessage,
CommitMetadata,
CommitTimestamp,
} from "@/components/ai-elements/commit"
import { DiffViewer } from "@/components/diff/diff-viewer"
import { Button } from "@/components/ui/button"
import { gitLog, gitPush, gitShowFile } from "@/lib/tauri"
import { toErrorMessage } from "@/lib/app-error"
import { languageFromPath } from "@/lib/language-detect"
import type { GitLogEntry, GitLogFileChange } from "@/lib/types"
import { useGitCredential } from "@/contexts/git-credential-context"
// --- File tree types & builder (same as aux-panel-git-log-tab) ---
type CommitFileTreeDirNode = {
kind: "dir"
name: string
path: string
children: CommitFileTreeNode[]
fileCount: number
}
type CommitFileTreeFileNode = {
kind: "file"
name: string
path: string
change: GitLogFileChange
}
type CommitFileTreeNode = CommitFileTreeDirNode | CommitFileTreeFileNode
interface MutableCommitFileTreeDirNode {
kind: "dir"
name: string
path: string
children: Map<string, MutableCommitFileTreeDirNode | CommitFileTreeFileNode>
}
function normalizePathSegments(path: string): string[] {
const normalized = path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")
if (!normalized) return []
return normalized.split("/").filter(Boolean)
}
function toSortedTreeNodes(
dir: MutableCommitFileTreeDirNode
): CommitFileTreeNode[] {
return Array.from(dir.children.values())
.map<CommitFileTreeNode>((node) => {
if (node.kind === "file") return node
return {
kind: "dir" as const,
fileCount: 0,
name: node.name,
path: node.path,
children: toSortedTreeNodes(node),
}
})
.sort((a, b) => {
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
})
}
function compressAndAnnotateDir(
node: CommitFileTreeDirNode
): CommitFileTreeDirNode {
let compressedChildren: CommitFileTreeNode[] = node.children.map((child) => {
if (child.kind === "file") return child
return compressAndAnnotateDir(child)
})
let fileCount = compressedChildren.reduce((count, child) => {
if (child.kind === "file") return count + 1
return count + child.fileCount
}, 0)
let nextNode: CommitFileTreeDirNode = {
...node,
children: compressedChildren,
fileCount,
}
while (
nextNode.children.length === 1 &&
nextNode.children[0].kind === "dir"
) {
const onlyChild = nextNode.children[0]
nextNode = {
kind: "dir",
name: `${nextNode.name}/${onlyChild.name}`,
path: onlyChild.path,
children: onlyChild.children,
fileCount: onlyChild.fileCount,
}
}
compressedChildren = nextNode.children
fileCount = compressedChildren.reduce((count, child) => {
if (child.kind === "file") return count + 1
return count + child.fileCount
}, 0)
return {
...nextNode,
children: compressedChildren,
fileCount,
}
}
function buildCommitFileTree(files: GitLogFileChange[]): CommitFileTreeNode[] {
const root: MutableCommitFileTreeDirNode = {
kind: "dir",
name: "",
path: "",
children: new Map(),
}
for (const change of files) {
const segments = normalizePathSegments(change.path)
if (segments.length === 0) continue
let current = root
for (const [index, segment] of segments.entries()) {
const nodePath = segments.slice(0, index + 1).join("/")
const isLeaf = index === segments.length - 1
if (isLeaf) {
current.children.set(`file:${nodePath}`, {
kind: "file",
name: segment,
path: nodePath,
change,
})
continue
}
const dirKey = `dir:${nodePath}`
const existing = current.children.get(dirKey)
if (existing && existing.kind === "dir") {
current = existing
continue
}
const nextDir: MutableCommitFileTreeDirNode = {
kind: "dir",
name: segment,
path: nodePath,
children: new Map(),
}
current.children.set(dirKey, nextDir)
current = nextDir
}
}
const sortedNodes = toSortedTreeNodes(root)
return sortedNodes.map((node) => {
if (node.kind === "file") return node
return compressAndAnnotateDir(node)
})
}
function collectExpandedDirectoryPaths(
nodes: CommitFileTreeNode[],
expanded = new Set<string>()
): Set<string> {
for (const node of nodes) {
if (node.kind !== "dir") continue
expanded.add(node.path)
collectExpandedDirectoryPaths(node.children, expanded)
}
return expanded
}
function mapFileStatus(
status: string
): "added" | "modified" | "deleted" | "renamed" {
switch (status.toUpperCase().charAt(0)) {
case "A":
return "added"
case "D":
return "deleted"
case "R":
return "renamed"
default:
return "modified"
}
}
function formatRelativeTime(
dateStr: string,
t: (
key:
| "time.monthsAgo"
| "time.daysAgo"
| "time.hoursAgo"
| "time.minsAgo"
| "time.justNow",
values?: { count: number }
) => string
): string {
const date = new Date(dateStr)
if (Number.isNaN(date.getTime())) return dateStr
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMin = Math.floor(diffMs / 60_000)
const diffHour = Math.floor(diffMin / 60)
const diffDay = Math.floor(diffHour / 24)
if (diffDay > 30) {
const diffMonth = Math.floor(diffDay / 30)
return t("time.monthsAgo", { count: diffMonth })
}
if (diffDay > 0) return t("time.daysAgo", { count: diffDay })
if (diffHour > 0) return t("time.hoursAgo", { count: diffHour })
if (diffMin > 0) return t("time.minsAgo", { count: diffMin })
return t("time.justNow", { count: 0 })
}
function parseDate(dateStr: string): Date | null {
const date = new Date(dateStr)
return Number.isNaN(date.getTime()) ? null : date
}
// --- Main component ---
interface PushWorkspaceProps {
folderPath: string
folderName: string
}
export function PushWorkspace({ folderPath, folderName }: PushWorkspaceProps) {
const t = useTranslations("Folder.pushWindow")
const tLog = useTranslations("Folder.gitLogTab")
const { withCredentialRetry } = useGitCredential()
const [commits, setCommits] = useState<GitLogEntry[]>([])
const [listLoading, setListLoading] = useState(false)
const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({})
const [pushing, setPushing] = useState(false)
const [selectedFile, setSelectedFile] = useState<string | null>(null)
const [selectedCommit, setSelectedCommit] = useState<string | null>(null)
const [originalContent, setOriginalContent] = useState("")
const [modifiedContent, setModifiedContent] = useState("")
const [diffLoading, setDiffLoading] = useState(false)
const unpushedCommits = useMemo(
() => commits.filter((c) => c.pushed === false),
[commits]
)
const loadCommits = useCallback(async () => {
setListLoading(true)
try {
const entries = await gitLog(folderPath, 100)
setCommits(entries)
} catch (err) {
toast.error(toErrorMessage(err))
} finally {
setListLoading(false)
}
}, [folderPath])
useEffect(() => {
loadCommits()
}, [loadCommits])
async function handleSelectFile(commitHash: string, file: string) {
setSelectedFile(file)
setSelectedCommit(commitHash)
setDiffLoading(true)
try {
const [orig, mod] = await Promise.all([
gitShowFile(folderPath, file, `${commitHash}~1`).catch(() => ""),
gitShowFile(folderPath, file, commitHash).catch(() => ""),
])
setOriginalContent(orig)
setModifiedContent(mod)
} catch {
setOriginalContent("")
setModifiedContent("")
} finally {
setDiffLoading(false)
}
}
async function handlePush() {
setPushing(true)
try {
const result = await withCredentialRetry(
(creds) => gitPush(folderPath, creds),
{ folderPath }
)
let description: string | undefined
if (result.upstream_set) {
description =
result.pushed_commits === 0
? t("toasts.upstreamSet")
: t("toasts.upstreamSetAndPushed", {
count: result.pushed_commits,
})
} else if (result.pushed_commits === 0) {
description = t("toasts.noCommitsToPush")
} else {
description = t("toasts.pushedCommits", {
count: result.pushed_commits,
})
}
toast.success(t("toasts.pushSuccess"), { description })
await loadCommits()
setSelectedFile(null)
setSelectedCommit(null)
setOpenByCommit({})
} catch (err) {
toast.error(t("toasts.pushFailed"), {
description: toErrorMessage(err),
})
} finally {
setPushing(false)
}
}
return (
<div className="flex h-full flex-col">
<ResizablePanelGroup direction="horizontal" className="min-h-0 flex-1">
{/* Left panel: commit list */}
<ResizablePanel defaultSize={35} minSize={25}>
<div className="flex h-full flex-col">
<ScrollArea className="min-h-0 flex-1">
{listLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : unpushedCommits.length === 0 ? (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
{t("noUnpushedCommits")}
</div>
) : (
<div className="flex flex-col gap-2 p-2">
{unpushedCommits.map((entry) => {
const commitKey = entry.full_hash
const commitDate = parseDate(entry.date)
const isOpen = !!openByCommit[commitKey]
return (
<Commit
key={commitKey}
open={isOpen}
onOpenChange={(open) => {
setOpenByCommit((prev) => ({
...prev,
[commitKey]: open,
}))
}}
>
<CommitHeader>
<CommitInfo className="min-w-0">
<CommitMessage className="line-clamp-1 leading-snug">
{entry.message}
</CommitMessage>
<CommitMetadata className="mt-1 min-w-0 flex items-center gap-1.5">
<span
className="inline-flex shrink-0"
title={t("unpushed")}
>
<CloudOff
className="text-amber-500"
size={12}
/>
</span>
<span className="truncate">{entry.author}</span>
<CommitTimestamp
className="shrink-0"
date={commitDate ?? new Date()}
>
{formatRelativeTime(entry.date, tLog)}
</CommitTimestamp>
<CommitHash className="text-primary/70">
{entry.hash}
</CommitHash>
</CommitMetadata>
</CommitInfo>
</CommitHeader>
<CommitContent>
{entry.files.length === 0 ? (
<p className="text-xs text-muted-foreground">
{tLog("noFileChangeDetails")}
</p>
) : (
<PushCommitFilesTree
commitHash={entry.full_hash}
files={entry.files}
folderName={folderName}
onSelectFile={(file) =>
handleSelectFile(entry.full_hash, file)
}
/>
)}
</CommitContent>
</Commit>
)
})}
</div>
)}
</ScrollArea>
{/* Push button */}
<div className="border-t p-2">
<Button
className="w-full"
disabled={pushing || unpushedCommits.length === 0}
onClick={handlePush}
>
{pushing ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Upload className="mr-2 h-4 w-4" />
)}
{t("push")}
{unpushedCommits.length > 0 && ` (${unpushedCommits.length})`}
</Button>
</div>
</div>
</ResizablePanel>
<ResizableHandle />
{/* Right panel: diff viewer */}
<ResizablePanel defaultSize={65} minSize={40}>
{diffLoading ? (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : selectedFile && selectedCommit ? (
<DiffViewer
original={originalContent}
modified={modifiedContent}
originalLabel={`${selectedCommit.slice(0, 7)}~ (${t("before")})`}
modifiedLabel={`${selectedCommit.slice(0, 7)} (${t("after")})`}
language={languageFromPath(selectedFile)}
className="h-full"
/>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
{t("selectFileToViewDiff")}
</div>
)}
</ResizablePanel>
</ResizablePanelGroup>
</div>
)
}
// --- Commit Files Tree for Push Window ---
function PushCommitFilesTree({
commitHash,
files,
folderName,
onSelectFile,
}: {
commitHash: string
files: GitLogFileChange[]
folderName: string
onSelectFile: (file: string) => void
}) {
const tLog = useTranslations("Folder.gitLogTab")
const rootPath = "__push_file_tree_root__"
const treeNodes = useMemo(() => buildCommitFileTree(files), [files])
const allDirectoryPaths = useMemo(() => {
const paths = collectExpandedDirectoryPaths(treeNodes)
paths.add(rootPath)
return paths
}, [treeNodes])
const [expandedPaths, setExpandedPaths] =
useState<Set<string>>(allDirectoryPaths)
useEffect(() => {
setExpandedPaths(allDirectoryPaths)
}, [allDirectoryPaths])
const canExpandAll = useMemo(() => {
if (allDirectoryPaths.size === 0) return false
for (const path of allDirectoryPaths) {
if (!expandedPaths.has(path)) return true
}
return false
}, [allDirectoryPaths, expandedPaths])
const canCollapseAll = expandedPaths.size > 0
const toggleExpanded = useCallback(() => {
if (canExpandAll) {
setExpandedPaths(new Set(allDirectoryPaths))
return
}
setExpandedPaths(new Set())
}, [allDirectoryPaths, canExpandAll])
const renderNode = (node: CommitFileTreeNode): ReactElement => {
if (node.kind === "dir") {
return (
<FileTreeFolder
key={node.path}
path={node.path}
name={node.name}
suffix={`(${node.fileCount})`}
suffixClassName="text-muted-foreground/45"
title={node.path}
>
{node.children.map(renderNode)}
</FileTreeFolder>
)
}
const file = node.change
return (
<FileTreeFile
key={`${commitHash}:${file.path}`}
className="w-full min-w-0 cursor-pointer"
name={node.name}
onClick={() => onSelectFile(file.path)}
path={node.path}
title={file.path}
>
<>
<span className="size-4 shrink-0" />
<CommitFileInfo className="flex-1 min-w-0 gap-1.5">
<CommitFileStatus status={mapFileStatus(file.status)}>
{file.status}
</CommitFileStatus>
<CommitFileIcon />
<CommitFilePath title={file.path}>{node.name}</CommitFilePath>
</CommitFileInfo>
<CommitFileChanges>
<CommitFileAdditions count={file.additions} />
<CommitFileDeletions count={file.deletions} />
</CommitFileChanges>
</>
</FileTreeFile>
)
}
return (
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<p className="text-[11px] text-muted-foreground">
{tLog("filesTitle")}
</p>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="size-5"
onClick={toggleExpanded}
disabled={!canExpandAll && !canCollapseAll}
title={
canExpandAll ? tLog("expandAllFiles") : tLog("collapseAllFiles")
}
>
{canExpandAll ? (
<ChevronsUpDown className="size-3.5" />
) : (
<ChevronsDownUp className="size-3.5" />
)}
</Button>
</div>
</div>
<CommitFiles>
<FileTree
className="max-h-[32rem] overflow-auto rounded-md border-border/60 bg-transparent text-xs [&>div]:p-1"
expanded={expandedPaths}
onExpandedChange={setExpandedPaths}
>
<FileTreeFolder
path={rootPath}
name={folderName}
suffix={`(${files.length})`}
suffixClassName="text-muted-foreground/45"
title={folderName}
>
{treeNodes.map(renderNode)}
</FileTreeFolder>
</FileTree>
</CommitFiles>
</div>
)
}

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "كل الملفات محدثة", "allFilesUpToDate": "كل الملفات محدثة",
"updatedFiles": "{count, plural, one {# ملف تم تحديثه} other {# ملفات تم تحديثها}}", "updatedFiles": "{count, plural, one {# ملف تم تحديثه} other {# ملفات تم تحديثها}}",
"openCommitWindowFailed": "فشل فتح نافذة الالتزام", "openCommitWindowFailed": "فشل فتح نافذة الالتزام",
"openPushWindowFailed": "فشل فتح نافذة الدفع",
"upstreamSet": "تم تعيين فرع upstream", "upstreamSet": "تم تعيين فرع upstream",
"upstreamSetAndPushed": "تم تعيين فرع upstream ودفع {count, plural, one {# التزام} other {# التزامات}}", "upstreamSetAndPushed": "تم تعيين فرع upstream ودفع {count, plural, one {# التزام} other {# التزامات}}",
"noCommitsToPush": "لا توجد التزامات للدفع", "noCommitsToPush": "لا توجد التزامات للدفع",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "انقر اسم الملف لعرض الفرق", "clickFileToDiff": "انقر اسم الملف لعرض الفرق",
"loadingDiff": "جارٍ تحميل diff..." "loadingDiff": "جارٍ تحميل diff..."
}, },
"pushWindow": {
"title": "دفع الكود",
"noUnpushedCommits": "لا توجد التزامات غير مدفوعة",
"unpushed": "غير مدفوع",
"selectFileToViewDiff": "اختر ملفًا لعرض الفرق",
"before": "قبل",
"after": "بعد",
"push": "دفع",
"toasts": {
"pushSuccess": "تم الدفع بنجاح",
"pushFailed": "فشل الدفع",
"upstreamSet": "تم تعيين الفرع البعيد",
"upstreamSetAndPushed": "تم تعيين الفرع البعيد ودفع {count} التزام",
"noCommitsToPush": "لا توجد التزامات للدفع",
"pushedCommits": "تم دفع {count} التزام"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "الملفات", "filesTitle": "الملفات",
"expandAllFiles": "توسيع كل الملفات", "expandAllFiles": "توسيع كل الملفات",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "Alle Dateien sind aktuell", "allFilesUpToDate": "Alle Dateien sind aktuell",
"updatedFiles": "{count, plural, one {# Datei aktualisiert} other {# Dateien aktualisiert}}", "updatedFiles": "{count, plural, one {# Datei aktualisiert} other {# Dateien aktualisiert}}",
"openCommitWindowFailed": "Commit-Fenster konnte nicht geöffnet werden", "openCommitWindowFailed": "Commit-Fenster konnte nicht geöffnet werden",
"openPushWindowFailed": "Push-Fenster konnte nicht geöffnet werden",
"upstreamSet": "Upstream-Branch wurde gesetzt", "upstreamSet": "Upstream-Branch wurde gesetzt",
"upstreamSetAndPushed": "Upstream-Branch gesetzt und {count, plural, one {# Commit} other {# Commits}} gepusht", "upstreamSetAndPushed": "Upstream-Branch gesetzt und {count, plural, one {# Commit} other {# Commits}} gepusht",
"noCommitsToPush": "Keine Commits zum Pushen", "noCommitsToPush": "Keine Commits zum Pushen",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "Dateinamen anklicken, um Diff zu sehen", "clickFileToDiff": "Dateinamen anklicken, um Diff zu sehen",
"loadingDiff": "Diff wird geladen..." "loadingDiff": "Diff wird geladen..."
}, },
"pushWindow": {
"title": "Code pushen",
"noUnpushedCommits": "Keine ungepushten Commits",
"unpushed": "Nicht gepusht",
"selectFileToViewDiff": "Datei auswählen, um Unterschiede anzuzeigen",
"before": "Vorher",
"after": "Nachher",
"push": "Pushen",
"toasts": {
"pushSuccess": "Push erfolgreich",
"pushFailed": "Push fehlgeschlagen",
"upstreamSet": "Remote-Tracking-Branch wurde eingerichtet",
"upstreamSetAndPushed": "Remote-Tracking-Branch eingerichtet und {count} Commits gepusht",
"noCommitsToPush": "Keine Commits zum Pushen",
"pushedCommits": "{count} Commits gepusht"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "Dateien", "filesTitle": "Dateien",
"expandAllFiles": "Alle Dateien ausklappen", "expandAllFiles": "Alle Dateien ausklappen",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "All files are up to date", "allFilesUpToDate": "All files are up to date",
"updatedFiles": "Updated {count, plural, one {# file} other {# files}}", "updatedFiles": "Updated {count, plural, one {# file} other {# files}}",
"openCommitWindowFailed": "Failed to open commit window", "openCommitWindowFailed": "Failed to open commit window",
"openPushWindowFailed": "Failed to open push window",
"upstreamSet": "Upstream branch has been set", "upstreamSet": "Upstream branch has been set",
"upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}", "upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}",
"noCommitsToPush": "No commits to push", "noCommitsToPush": "No commits to push",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "Click a file name to view diff", "clickFileToDiff": "Click a file name to view diff",
"loadingDiff": "Loading diff..." "loadingDiff": "Loading diff..."
}, },
"pushWindow": {
"title": "Push Code",
"noUnpushedCommits": "No unpushed commits",
"unpushed": "Unpushed",
"selectFileToViewDiff": "Select a file to view diff",
"before": "Before",
"after": "After",
"push": "Push",
"toasts": {
"pushSuccess": "Push successful",
"pushFailed": "Push failed",
"upstreamSet": "Upstream branch has been set",
"upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}",
"noCommitsToPush": "No commits to push",
"pushedCommits": "Pushed {count, plural, one {# commit} other {# commits}}"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "Files", "filesTitle": "Files",
"expandAllFiles": "Expand all files", "expandAllFiles": "Expand all files",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "Todos los archivos están actualizados", "allFilesUpToDate": "Todos los archivos están actualizados",
"updatedFiles": "{count, plural, one {# archivo actualizado} other {# archivos actualizados}}", "updatedFiles": "{count, plural, one {# archivo actualizado} other {# archivos actualizados}}",
"openCommitWindowFailed": "No se pudo abrir la ventana de commit", "openCommitWindowFailed": "No se pudo abrir la ventana de commit",
"openPushWindowFailed": "Error al abrir la ventana de envío",
"upstreamSet": "La rama upstream se ha configurado", "upstreamSet": "La rama upstream se ha configurado",
"upstreamSetAndPushed": "Rama upstream configurada y se enviaron {count, plural, one {# commit} other {# commits}}", "upstreamSetAndPushed": "Rama upstream configurada y se enviaron {count, plural, one {# commit} other {# commits}}",
"noCommitsToPush": "No hay commits para enviar", "noCommitsToPush": "No hay commits para enviar",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "Haz clic en un nombre de archivo para ver diff", "clickFileToDiff": "Haz clic en un nombre de archivo para ver diff",
"loadingDiff": "Cargando diff..." "loadingDiff": "Cargando diff..."
}, },
"pushWindow": {
"title": "Enviar código",
"noUnpushedCommits": "No hay commits sin enviar",
"unpushed": "Sin enviar",
"selectFileToViewDiff": "Selecciona un archivo para ver las diferencias",
"before": "Antes",
"after": "Después",
"push": "Enviar",
"toasts": {
"pushSuccess": "Envío exitoso",
"pushFailed": "Error al enviar",
"upstreamSet": "Se ha configurado la rama remota",
"upstreamSetAndPushed": "Rama remota configurada y enviados {count} commits",
"noCommitsToPush": "No hay commits para enviar",
"pushedCommits": "Enviados {count} commits"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "Archivos", "filesTitle": "Archivos",
"expandAllFiles": "Expandir todos los archivos", "expandAllFiles": "Expandir todos los archivos",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "Tous les fichiers sont à jour", "allFilesUpToDate": "Tous les fichiers sont à jour",
"updatedFiles": "{count, plural, one {# fichier mis à jour} other {# fichiers mis à jour}}", "updatedFiles": "{count, plural, one {# fichier mis à jour} other {# fichiers mis à jour}}",
"openCommitWindowFailed": "Impossible douvrir la fenêtre de commit", "openCommitWindowFailed": "Impossible douvrir la fenêtre de commit",
"openPushWindowFailed": "Impossible douvrir la fenêtre de push",
"upstreamSet": "La branche upstream a été définie", "upstreamSet": "La branche upstream a été définie",
"upstreamSetAndPushed": "Branche upstream définie et {count, plural, one {# commit} other {# commits}} poussé(s)", "upstreamSetAndPushed": "Branche upstream définie et {count, plural, one {# commit} other {# commits}} poussé(s)",
"noCommitsToPush": "Aucun commit à pousser", "noCommitsToPush": "Aucun commit à pousser",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "Cliquez sur un nom de fichier pour voir le diff", "clickFileToDiff": "Cliquez sur un nom de fichier pour voir le diff",
"loadingDiff": "Chargement du diff..." "loadingDiff": "Chargement du diff..."
}, },
"pushWindow": {
"title": "Pousser le code",
"noUnpushedCommits": "Aucun commit non poussé",
"unpushed": "Non poussé",
"selectFileToViewDiff": "Sélectionnez un fichier pour voir les différences",
"before": "Avant",
"after": "Après",
"push": "Pousser",
"toasts": {
"pushSuccess": "Push réussi",
"pushFailed": "Échec du push",
"upstreamSet": "La branche distante a été configurée",
"upstreamSetAndPushed": "Branche distante configurée et {count} commits poussés",
"noCommitsToPush": "Aucun commit à pousser",
"pushedCommits": "{count} commits poussés"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "Fichiers", "filesTitle": "Fichiers",
"expandAllFiles": "Développer tous les fichiers", "expandAllFiles": "Développer tous les fichiers",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "すべてのファイルは最新です", "allFilesUpToDate": "すべてのファイルは最新です",
"updatedFiles": "{count, plural, one {# 個のファイルを更新} other {# 個のファイルを更新}}", "updatedFiles": "{count, plural, one {# 個のファイルを更新} other {# 個のファイルを更新}}",
"openCommitWindowFailed": "コミットウィンドウを開けませんでした", "openCommitWindowFailed": "コミットウィンドウを開けませんでした",
"openPushWindowFailed": "プッシュウィンドウを開けませんでした",
"upstreamSet": "アップストリームブランチを設定しました", "upstreamSet": "アップストリームブランチを設定しました",
"upstreamSetAndPushed": "アップストリームブランチを設定し、{count, plural, one {# 件のコミット} other {# 件のコミット}}をプッシュしました", "upstreamSetAndPushed": "アップストリームブランチを設定し、{count, plural, one {# 件のコミット} other {# 件のコミット}}をプッシュしました",
"noCommitsToPush": "プッシュするコミットはありません", "noCommitsToPush": "プッシュするコミットはありません",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "ファイル名をクリックして差分を表示", "clickFileToDiff": "ファイル名をクリックして差分を表示",
"loadingDiff": "差分を読み込み中..." "loadingDiff": "差分を読み込み中..."
}, },
"pushWindow": {
"title": "コードをプッシュ",
"noUnpushedCommits": "未プッシュのコミットはありません",
"unpushed": "未プッシュ",
"selectFileToViewDiff": "ファイルを選択して差分を表示",
"before": "変更前",
"after": "変更後",
"push": "プッシュ",
"toasts": {
"pushSuccess": "プッシュ成功",
"pushFailed": "プッシュ失敗",
"upstreamSet": "リモート追跡ブランチが設定されました",
"upstreamSetAndPushed": "リモート追跡ブランチを設定し、{count}件のコミットをプッシュしました",
"noCommitsToPush": "プッシュするコミットはありません",
"pushedCommits": "{count}件のコミットをプッシュしました"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "ファイル", "filesTitle": "ファイル",
"expandAllFiles": "すべてのファイルを展開", "expandAllFiles": "すべてのファイルを展開",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "모든 파일이 최신 상태입니다", "allFilesUpToDate": "모든 파일이 최신 상태입니다",
"updatedFiles": "{count, plural, one {#개 파일 업데이트됨} other {#개 파일 업데이트됨}}", "updatedFiles": "{count, plural, one {#개 파일 업데이트됨} other {#개 파일 업데이트됨}}",
"openCommitWindowFailed": "커밋 창을 열지 못했습니다", "openCommitWindowFailed": "커밋 창을 열지 못했습니다",
"openPushWindowFailed": "푸시 창 열기 실패",
"upstreamSet": "업스트림 브랜치가 설정되었습니다", "upstreamSet": "업스트림 브랜치가 설정되었습니다",
"upstreamSetAndPushed": "업스트림 브랜치를 설정하고 {count, plural, one {#개 커밋} other {#개 커밋}}을 푸시했습니다", "upstreamSetAndPushed": "업스트림 브랜치를 설정하고 {count, plural, one {#개 커밋} other {#개 커밋}}을 푸시했습니다",
"noCommitsToPush": "푸시할 커밋이 없습니다", "noCommitsToPush": "푸시할 커밋이 없습니다",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "파일 이름을 클릭해 diff를 확인하세요", "clickFileToDiff": "파일 이름을 클릭해 diff를 확인하세요",
"loadingDiff": "diff 로딩 중..." "loadingDiff": "diff 로딩 중..."
}, },
"pushWindow": {
"title": "코드 푸시",
"noUnpushedCommits": "푸시되지 않은 커밋이 없습니다",
"unpushed": "미푸시",
"selectFileToViewDiff": "파일을 선택하여 차이 보기",
"before": "변경 전",
"after": "변경 후",
"push": "푸시",
"toasts": {
"pushSuccess": "푸시 성공",
"pushFailed": "푸시 실패",
"upstreamSet": "원격 추적 브랜치가 설정되었습니다",
"upstreamSetAndPushed": "원격 추적 브랜치 설정 및 {count}개 커밋 푸시 완료",
"noCommitsToPush": "푸시할 커밋이 없습니다",
"pushedCommits": "{count}개 커밋 푸시 완료"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "파일", "filesTitle": "파일",
"expandAllFiles": "모든 파일 펼치기", "expandAllFiles": "모든 파일 펼치기",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "Todos os arquivos estão atualizados", "allFilesUpToDate": "Todos os arquivos estão atualizados",
"updatedFiles": "{count, plural, one {# arquivo atualizado} other {# arquivos atualizados}}", "updatedFiles": "{count, plural, one {# arquivo atualizado} other {# arquivos atualizados}}",
"openCommitWindowFailed": "Falha ao abrir a janela de commit", "openCommitWindowFailed": "Falha ao abrir a janela de commit",
"openPushWindowFailed": "Falha ao abrir janela de envio",
"upstreamSet": "A branch upstream foi definida", "upstreamSet": "A branch upstream foi definida",
"upstreamSetAndPushed": "Branch upstream definida e {count, plural, one {# commit} other {# commits}} enviado(s)", "upstreamSetAndPushed": "Branch upstream definida e {count, plural, one {# commit} other {# commits}} enviado(s)",
"noCommitsToPush": "Não há commits para enviar", "noCommitsToPush": "Não há commits para enviar",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "Clique no nome do arquivo para ver o diff", "clickFileToDiff": "Clique no nome do arquivo para ver o diff",
"loadingDiff": "Carregando diff..." "loadingDiff": "Carregando diff..."
}, },
"pushWindow": {
"title": "Enviar código",
"noUnpushedCommits": "Nenhum commit não enviado",
"unpushed": "Não enviado",
"selectFileToViewDiff": "Selecione um arquivo para ver as diferenças",
"before": "Antes",
"after": "Depois",
"push": "Enviar",
"toasts": {
"pushSuccess": "Envio bem-sucedido",
"pushFailed": "Falha no envio",
"upstreamSet": "Branch remoto foi configurado",
"upstreamSetAndPushed": "Branch remoto configurado e {count} commits enviados",
"noCommitsToPush": "Nenhum commit para enviar",
"pushedCommits": "{count} commits enviados"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "Arquivos", "filesTitle": "Arquivos",
"expandAllFiles": "Expandir todos os arquivos", "expandAllFiles": "Expandir todos os arquivos",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "所有文件均为最新版本", "allFilesUpToDate": "所有文件均为最新版本",
"updatedFiles": "已更新 {count} 个文件", "updatedFiles": "已更新 {count} 个文件",
"openCommitWindowFailed": "打开提交窗口失败", "openCommitWindowFailed": "打开提交窗口失败",
"openPushWindowFailed": "打开推送窗口失败",
"upstreamSet": "已设置远程跟踪分支", "upstreamSet": "已设置远程跟踪分支",
"upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交", "upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交",
"noCommitsToPush": "没有可推送的提交", "noCommitsToPush": "没有可推送的提交",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "点击文件名查看差异", "clickFileToDiff": "点击文件名查看差异",
"loadingDiff": "加载差异..." "loadingDiff": "加载差异..."
}, },
"pushWindow": {
"title": "推送代码",
"noUnpushedCommits": "没有未推送的提交",
"unpushed": "未推送",
"selectFileToViewDiff": "选择文件查看差异",
"before": "修改前",
"after": "修改后",
"push": "推送",
"toasts": {
"pushSuccess": "推送成功",
"pushFailed": "推送失败",
"upstreamSet": "已设置远程跟踪分支",
"upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交",
"noCommitsToPush": "没有可推送的提交",
"pushedCommits": "已推送 {count} 个提交"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "文件", "filesTitle": "文件",
"expandAllFiles": "展开全部文件", "expandAllFiles": "展开全部文件",

View File

@@ -882,6 +882,7 @@
"allFilesUpToDate": "所有檔案均為最新版本", "allFilesUpToDate": "所有檔案均為最新版本",
"updatedFiles": "已更新 {count} 個檔案", "updatedFiles": "已更新 {count} 個檔案",
"openCommitWindowFailed": "打開提交視窗失敗", "openCommitWindowFailed": "打開提交視窗失敗",
"openPushWindowFailed": "開啟推送視窗失敗",
"upstreamSet": "已設定遠端追蹤分支", "upstreamSet": "已設定遠端追蹤分支",
"upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交", "upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交",
"noCommitsToPush": "沒有可推送的提交", "noCommitsToPush": "沒有可推送的提交",
@@ -1032,6 +1033,23 @@
"clickFileToDiff": "點擊檔案名稱查看差異", "clickFileToDiff": "點擊檔案名稱查看差異",
"loadingDiff": "載入差異中..." "loadingDiff": "載入差異中..."
}, },
"pushWindow": {
"title": "推送程式碼",
"noUnpushedCommits": "沒有未推送的提交",
"unpushed": "未推送",
"selectFileToViewDiff": "選擇檔案查看差異",
"before": "修改前",
"after": "修改後",
"push": "推送",
"toasts": {
"pushSuccess": "推送成功",
"pushFailed": "推送失敗",
"upstreamSet": "已設定遠端追蹤分支",
"upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交",
"noCommitsToPush": "沒有可推送的提交",
"pushedCommits": "已推送 {count} 個提交"
}
},
"gitLogTab": { "gitLogTab": {
"filesTitle": "檔案", "filesTitle": "檔案",
"expandAllFiles": "展開全部檔案", "expandAllFiles": "展開全部檔案",

View File

@@ -661,6 +661,10 @@ export async function openStashWindow(folderId: number): Promise<void> {
return invoke("open_stash_window", { folderId }) return invoke("open_stash_window", { folderId })
} }
export async function openPushWindow(folderId: number): Promise<void> {
return invoke("open_push_window", { folderId })
}
export async function gitStashPush( export async function gitStashPush(
path: string, path: string,
message?: string, message?: string,