From d9032f1c82db45c02fdccbff138bace1f5bcff02 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 21 Mar 2026 20:37:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0git=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=EF=BC=8C=E6=98=BE=E7=A4=BA=E5=BE=85=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E5=88=97=E8=A1=A8=E5=92=8C=E6=9F=A5=E7=9C=8B=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=B7=AE=E5=BC=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/capabilities/default.json | 2 +- src-tauri/src/commands/windows.rs | 39 ++ src-tauri/src/lib.rs | 1 + src/app/push/layout.tsx | 8 + src/app/push/page.tsx | 111 ++++ src/components/layout/branch-dropdown.tsx | 150 +----- src/components/layout/push-workspace.tsx | 628 ++++++++++++++++++++++ src/i18n/messages/ar.json | 18 + src/i18n/messages/de.json | 18 + src/i18n/messages/en.json | 18 + src/i18n/messages/es.json | 18 + src/i18n/messages/fr.json | 18 + src/i18n/messages/ja.json | 18 + src/i18n/messages/ko.json | 18 + src/i18n/messages/pt.json | 18 + src/i18n/messages/zh-CN.json | 18 + src/i18n/messages/zh-TW.json | 18 + src/lib/tauri.ts | 4 + 18 files changed, 986 insertions(+), 137 deletions(-) create mode 100644 src/app/push/layout.tsx create mode 100644 src/app/push/page.tsx create mode 100644 src/components/layout/push-workspace.tsx diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 9381994..7cf0b32 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "settings"], + "windows": ["welcome", "folder-*", "commit-*", "merge-*", "stash-*", "push-*", "settings"], "permissions": [ "core:default", "core:window:default", diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 716e18b..bb8b028 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -597,3 +597,42 @@ pub async fn open_stash_window( 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(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f7b532d..ede0554 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -263,6 +263,7 @@ pub fn run() { windows::focus_folder_window, windows::open_merge_window, windows::open_stash_window, + windows::open_push_window, system_settings::get_system_proxy_settings, system_settings::update_system_proxy_settings, system_settings::get_system_language_settings, diff --git a/src/app/push/layout.tsx b/src/app/push/layout.tsx new file mode 100644 index 0000000..080185f --- /dev/null +++ b/src/app/push/layout.tsx @@ -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 {children} +} diff --git a/src/app/push/page.tsx b/src/app/push/page.tsx new file mode 100644 index 0000000..8d432bd --- /dev/null +++ b/src/app/push/page.tsx @@ -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({ + 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 ( +
+ + {t("title")} + {hasValidFolderId && folder ? ` · ${folder.name}` : ""} +
+ } + /> + +
+ {!hasValidFolderId ? ( +
+ Invalid folder ID +
+ ) : loading ? ( +
+ +
+ ) : error ? ( +
+ {error} +
+ ) : folder ? ( + + ) : null} +
+ + + + ) +} + +export default function PushPage() { + return ( + + + + ) +} diff --git a/src/components/layout/branch-dropdown.tsx b/src/components/layout/branch-dropdown.tsx index 2536162..8218f4d 100644 --- a/src/components/layout/branch-dropdown.tsx +++ b/src/components/layout/branch-dropdown.tsx @@ -67,7 +67,6 @@ import { gitInit, gitPull, gitFetch, - gitPush, gitNewBranch, gitWorktreeAdd, gitCheckout, @@ -78,9 +77,8 @@ import { openFolderWindow, openCommitWindow, setFolderParentBranch, - gitListConflicts, - gitHasMergeHead, openStashWindow, + openPushWindow, } from "@/lib/tauri" import { RemoteManageDialog } from "@/components/layout/remote-manage-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() { if (!parentBranch) return setConfirmAction({ type: "merge", branchName: parentBranch }) @@ -751,7 +617,19 @@ export function BranchDropdown({ {t("openCommitWindow")} - + { + 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 }) + }) + }} + > {t("pushCode")} diff --git a/src/components/layout/push-workspace.tsx b/src/components/layout/push-workspace.tsx new file mode 100644 index 0000000..ebf620e --- /dev/null +++ b/src/components/layout/push-workspace.tsx @@ -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 +} + +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((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() +): Set { + 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([]) + const [listLoading, setListLoading] = useState(false) + const [openByCommit, setOpenByCommit] = useState>({}) + const [pushing, setPushing] = useState(false) + + const [selectedFile, setSelectedFile] = useState(null) + const [selectedCommit, setSelectedCommit] = useState(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 ( +
+ + {/* Left panel: commit list */} + +
+ + {listLoading ? ( +
+ +
+ ) : unpushedCommits.length === 0 ? ( +
+ {t("noUnpushedCommits")} +
+ ) : ( +
+ {unpushedCommits.map((entry) => { + const commitKey = entry.full_hash + const commitDate = parseDate(entry.date) + const isOpen = !!openByCommit[commitKey] + + return ( + { + setOpenByCommit((prev) => ({ + ...prev, + [commitKey]: open, + })) + }} + > + + + + {entry.message} + + + + + + {entry.author} + + {formatRelativeTime(entry.date, tLog)} + + + {entry.hash} + + + + + + {entry.files.length === 0 ? ( +

+ {tLog("noFileChangeDetails")} +

+ ) : ( + + handleSelectFile(entry.full_hash, file) + } + /> + )} +
+
+ ) + })} +
+ )} +
+ + {/* Push button */} +
+ +
+
+
+ + + + {/* Right panel: diff viewer */} + + {diffLoading ? ( +
+ +
+ ) : selectedFile && selectedCommit ? ( + + ) : ( +
+ {t("selectFileToViewDiff")} +
+ )} +
+
+
+ ) +} + +// --- 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>(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 ( + + {node.children.map(renderNode)} + + ) + } + + const file = node.change + return ( + onSelectFile(file.path)} + path={node.path} + title={file.path} + > + <> + + + + {file.status} + + + {node.name} + + + + + + + + ) + } + + return ( +
+
+

+ {tLog("filesTitle")} +

+
+ +
+
+ + + + {treeNodes.map(renderNode)} + + + +
+ ) +} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index bd95f1e..188cf3e 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "كل الملفات محدثة", "updatedFiles": "{count, plural, one {# ملف تم تحديثه} other {# ملفات تم تحديثها}}", "openCommitWindowFailed": "فشل فتح نافذة الالتزام", + "openPushWindowFailed": "فشل فتح نافذة الدفع", "upstreamSet": "تم تعيين فرع upstream", "upstreamSetAndPushed": "تم تعيين فرع upstream ودفع {count, plural, one {# التزام} other {# التزامات}}", "noCommitsToPush": "لا توجد التزامات للدفع", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "انقر اسم الملف لعرض الفرق", "loadingDiff": "جارٍ تحميل diff..." }, + "pushWindow": { + "title": "دفع الكود", + "noUnpushedCommits": "لا توجد التزامات غير مدفوعة", + "unpushed": "غير مدفوع", + "selectFileToViewDiff": "اختر ملفًا لعرض الفرق", + "before": "قبل", + "after": "بعد", + "push": "دفع", + "toasts": { + "pushSuccess": "تم الدفع بنجاح", + "pushFailed": "فشل الدفع", + "upstreamSet": "تم تعيين الفرع البعيد", + "upstreamSetAndPushed": "تم تعيين الفرع البعيد ودفع {count} التزام", + "noCommitsToPush": "لا توجد التزامات للدفع", + "pushedCommits": "تم دفع {count} التزام" + } + }, "gitLogTab": { "filesTitle": "الملفات", "expandAllFiles": "توسيع كل الملفات", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 33b7820..1f61d54 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "Alle Dateien sind aktuell", "updatedFiles": "{count, plural, one {# Datei aktualisiert} other {# Dateien aktualisiert}}", "openCommitWindowFailed": "Commit-Fenster konnte nicht geöffnet werden", + "openPushWindowFailed": "Push-Fenster konnte nicht geöffnet werden", "upstreamSet": "Upstream-Branch wurde gesetzt", "upstreamSetAndPushed": "Upstream-Branch gesetzt und {count, plural, one {# Commit} other {# Commits}} gepusht", "noCommitsToPush": "Keine Commits zum Pushen", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "Dateinamen anklicken, um Diff zu sehen", "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": { "filesTitle": "Dateien", "expandAllFiles": "Alle Dateien ausklappen", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 81eb4d1..893d06c 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "All files are up to date", "updatedFiles": "Updated {count, plural, one {# file} other {# files}}", "openCommitWindowFailed": "Failed to open commit window", + "openPushWindowFailed": "Failed to open push window", "upstreamSet": "Upstream branch has been set", "upstreamSetAndPushed": "Upstream branch set and pushed {count, plural, one {# commit} other {# commits}}", "noCommitsToPush": "No commits to push", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "Click a file name to view 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": { "filesTitle": "Files", "expandAllFiles": "Expand all files", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 36c8470..1bbfee4 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "Todos los archivos están actualizados", "updatedFiles": "{count, plural, one {# archivo actualizado} other {# archivos actualizados}}", "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", "upstreamSetAndPushed": "Rama upstream configurada y se enviaron {count, plural, one {# commit} other {# commits}}", "noCommitsToPush": "No hay commits para enviar", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "Haz clic en un nombre de archivo para ver 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": { "filesTitle": "Archivos", "expandAllFiles": "Expandir todos los archivos", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 38b130e..a022b1f 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "Tous les fichiers sont à jour", "updatedFiles": "{count, plural, one {# fichier mis à jour} other {# fichiers mis à jour}}", "openCommitWindowFailed": "Impossible d’ouvrir la fenêtre de commit", + "openPushWindowFailed": "Impossible d’ouvrir la fenêtre de push", "upstreamSet": "La branche upstream a été définie", "upstreamSetAndPushed": "Branche upstream définie et {count, plural, one {# commit} other {# commits}} poussé(s)", "noCommitsToPush": "Aucun commit à pousser", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "Cliquez sur un nom de fichier pour voir le 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": { "filesTitle": "Fichiers", "expandAllFiles": "Développer tous les fichiers", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index a6f58e7..72bf3c4 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "すべてのファイルは最新です", "updatedFiles": "{count, plural, one {# 個のファイルを更新} other {# 個のファイルを更新}}", "openCommitWindowFailed": "コミットウィンドウを開けませんでした", + "openPushWindowFailed": "プッシュウィンドウを開けませんでした", "upstreamSet": "アップストリームブランチを設定しました", "upstreamSetAndPushed": "アップストリームブランチを設定し、{count, plural, one {# 件のコミット} other {# 件のコミット}}をプッシュしました", "noCommitsToPush": "プッシュするコミットはありません", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "ファイル名をクリックして差分を表示", "loadingDiff": "差分を読み込み中..." }, + "pushWindow": { + "title": "コードをプッシュ", + "noUnpushedCommits": "未プッシュのコミットはありません", + "unpushed": "未プッシュ", + "selectFileToViewDiff": "ファイルを選択して差分を表示", + "before": "変更前", + "after": "変更後", + "push": "プッシュ", + "toasts": { + "pushSuccess": "プッシュ成功", + "pushFailed": "プッシュ失敗", + "upstreamSet": "リモート追跡ブランチが設定されました", + "upstreamSetAndPushed": "リモート追跡ブランチを設定し、{count}件のコミットをプッシュしました", + "noCommitsToPush": "プッシュするコミットはありません", + "pushedCommits": "{count}件のコミットをプッシュしました" + } + }, "gitLogTab": { "filesTitle": "ファイル", "expandAllFiles": "すべてのファイルを展開", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index ddd26b3..8b972b1 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "모든 파일이 최신 상태입니다", "updatedFiles": "{count, plural, one {#개 파일 업데이트됨} other {#개 파일 업데이트됨}}", "openCommitWindowFailed": "커밋 창을 열지 못했습니다", + "openPushWindowFailed": "푸시 창 열기 실패", "upstreamSet": "업스트림 브랜치가 설정되었습니다", "upstreamSetAndPushed": "업스트림 브랜치를 설정하고 {count, plural, one {#개 커밋} other {#개 커밋}}을 푸시했습니다", "noCommitsToPush": "푸시할 커밋이 없습니다", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "파일 이름을 클릭해 diff를 확인하세요", "loadingDiff": "diff 로딩 중..." }, + "pushWindow": { + "title": "코드 푸시", + "noUnpushedCommits": "푸시되지 않은 커밋이 없습니다", + "unpushed": "미푸시", + "selectFileToViewDiff": "파일을 선택하여 차이 보기", + "before": "변경 전", + "after": "변경 후", + "push": "푸시", + "toasts": { + "pushSuccess": "푸시 성공", + "pushFailed": "푸시 실패", + "upstreamSet": "원격 추적 브랜치가 설정되었습니다", + "upstreamSetAndPushed": "원격 추적 브랜치 설정 및 {count}개 커밋 푸시 완료", + "noCommitsToPush": "푸시할 커밋이 없습니다", + "pushedCommits": "{count}개 커밋 푸시 완료" + } + }, "gitLogTab": { "filesTitle": "파일", "expandAllFiles": "모든 파일 펼치기", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index d06b1a2..63d17be 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "Todos os arquivos estão atualizados", "updatedFiles": "{count, plural, one {# arquivo atualizado} other {# arquivos atualizados}}", "openCommitWindowFailed": "Falha ao abrir a janela de commit", + "openPushWindowFailed": "Falha ao abrir janela de envio", "upstreamSet": "A branch upstream foi definida", "upstreamSetAndPushed": "Branch upstream definida e {count, plural, one {# commit} other {# commits}} enviado(s)", "noCommitsToPush": "Não há commits para enviar", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "Clique no nome do arquivo para ver o 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": { "filesTitle": "Arquivos", "expandAllFiles": "Expandir todos os arquivos", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 0c707ec..1cb4537 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "所有文件均为最新版本", "updatedFiles": "已更新 {count} 个文件", "openCommitWindowFailed": "打开提交窗口失败", + "openPushWindowFailed": "打开推送窗口失败", "upstreamSet": "已设置远程跟踪分支", "upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交", "noCommitsToPush": "没有可推送的提交", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "点击文件名查看差异", "loadingDiff": "加载差异..." }, + "pushWindow": { + "title": "推送代码", + "noUnpushedCommits": "没有未推送的提交", + "unpushed": "未推送", + "selectFileToViewDiff": "选择文件查看差异", + "before": "修改前", + "after": "修改后", + "push": "推送", + "toasts": { + "pushSuccess": "推送成功", + "pushFailed": "推送失败", + "upstreamSet": "已设置远程跟踪分支", + "upstreamSetAndPushed": "已设置远程跟踪分支并推送 {count} 个提交", + "noCommitsToPush": "没有可推送的提交", + "pushedCommits": "已推送 {count} 个提交" + } + }, "gitLogTab": { "filesTitle": "文件", "expandAllFiles": "展开全部文件", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index acd5468..9065901 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -882,6 +882,7 @@ "allFilesUpToDate": "所有檔案均為最新版本", "updatedFiles": "已更新 {count} 個檔案", "openCommitWindowFailed": "打開提交視窗失敗", + "openPushWindowFailed": "開啟推送視窗失敗", "upstreamSet": "已設定遠端追蹤分支", "upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交", "noCommitsToPush": "沒有可推送的提交", @@ -1032,6 +1033,23 @@ "clickFileToDiff": "點擊檔案名稱查看差異", "loadingDiff": "載入差異中..." }, + "pushWindow": { + "title": "推送程式碼", + "noUnpushedCommits": "沒有未推送的提交", + "unpushed": "未推送", + "selectFileToViewDiff": "選擇檔案查看差異", + "before": "修改前", + "after": "修改後", + "push": "推送", + "toasts": { + "pushSuccess": "推送成功", + "pushFailed": "推送失敗", + "upstreamSet": "已設定遠端追蹤分支", + "upstreamSetAndPushed": "已設定遠端追蹤分支並推送 {count} 個提交", + "noCommitsToPush": "沒有可推送的提交", + "pushedCommits": "已推送 {count} 個提交" + } + }, "gitLogTab": { "filesTitle": "檔案", "expandAllFiles": "展開全部檔案", diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 81353e9..4589a29 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -661,6 +661,10 @@ export async function openStashWindow(folderId: number): Promise { return invoke("open_stash_window", { folderId }) } +export async function openPushWindow(folderId: number): Promise { + return invoke("open_push_window", { folderId }) +} + export async function gitStashPush( path: string, message?: string,