From 767d43b0cfb7523266291e389c40af2f4f7f3e32 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Fri, 13 Mar 2026 22:55:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=94=AF=E6=8C=81=E8=BF=9C?= =?UTF-8?q?=E7=A8=8BGit=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/folders.rs | 111 ++++++++ src-tauri/src/lib.rs | 5 + src/components/layout/branch-dropdown.tsx | 49 ++++ .../layout/remote-manage-dialog.tsx | 250 ++++++++++++++++++ src/i18n/messages/ar.json | 9 +- src/i18n/messages/de.json | 9 +- src/i18n/messages/en.json | 9 +- src/i18n/messages/es.json | 9 +- src/i18n/messages/fr.json | 9 +- src/i18n/messages/ja.json | 9 +- src/i18n/messages/ko.json | 9 +- src/i18n/messages/pt.json | 9 +- src/i18n/messages/zh-CN.json | 9 +- src/i18n/messages/zh-TW.json | 9 +- src/lib/tauri.ts | 35 +++ src/lib/types.ts | 5 + 16 files changed, 535 insertions(+), 10 deletions(-) create mode 100644 src/components/layout/remote-manage-dialog.tsx diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index b790a04..875cd61 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -54,6 +54,12 @@ pub struct GitCommitResult { pub committed_files: usize, } +#[derive(Debug, Serialize)] +pub struct GitRemote { + pub name: String, + pub url: String, +} + #[derive(Debug, Clone, Serialize)] struct GitCommitSucceededEvent { folder_id: i32, @@ -1100,6 +1106,111 @@ pub async fn git_list_all_branches(path: String) -> Result Result, AppCommandError> { + let output = crate::process::tokio_command("git") + .args(["remote", "-v"]) + .current_dir(&path) + .output() + .await + .map_err(AppCommandError::io)?; + + if !output.status.success() { + return Err(git_command_error("remote -v", &output.stderr)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut seen = HashSet::new(); + let mut remotes = Vec::new(); + for line in stdout.lines() { + // Format: "name\turl (fetch|push)" + if !line.ends_with("(fetch)") { + continue; + } + let Some((name, rest)) = line.split_once('\t') else { + continue; + }; + let url = rest.trim_end_matches("(fetch)").trim(); + if seen.insert(name.to_string()) { + remotes.push(GitRemote { + name: name.to_string(), + url: url.to_string(), + }); + } + } + Ok(remotes) +} + +#[tauri::command] +pub async fn git_fetch_remote(path: String, name: String) -> Result { + let output = crate::process::tokio_command("git") + .args(["fetch", &name]) + .current_dir(&path) + .env("GIT_TERMINAL_PROMPT", "0") + .stdin(std::process::Stdio::null()) + .output() + .await + .map_err(AppCommandError::io)?; + + if !output.status.success() { + return Err(git_command_error("fetch", &output.stderr)); + } + Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) +} + +#[tauri::command] +pub async fn git_add_remote( + path: String, + name: String, + url: String, +) -> Result<(), AppCommandError> { + let output = crate::process::tokio_command("git") + .args(["remote", "add", &name, &url]) + .current_dir(&path) + .output() + .await + .map_err(AppCommandError::io)?; + + if !output.status.success() { + return Err(git_command_error("remote add", &output.stderr)); + } + Ok(()) +} + +#[tauri::command] +pub async fn git_remove_remote(path: String, name: String) -> Result<(), AppCommandError> { + let output = crate::process::tokio_command("git") + .args(["remote", "remove", &name]) + .current_dir(&path) + .output() + .await + .map_err(AppCommandError::io)?; + + if !output.status.success() { + return Err(git_command_error("remote remove", &output.stderr)); + } + Ok(()) +} + +#[tauri::command] +pub async fn git_set_remote_url( + path: String, + name: String, + url: String, +) -> Result<(), AppCommandError> { + let output = crate::process::tokio_command("git") + .args(["remote", "set-url", &name, &url]) + .current_dir(&path) + .output() + .await + .map_err(AppCommandError::io)?; + + if !output.status.success() { + return Err(git_command_error("remote set-url", &output.stderr)); + } + Ok(()) +} + #[tauri::command] pub async fn git_merge( path: String, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 891e85c..1638c21 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -199,6 +199,11 @@ pub fn run() { folders::git_rollback_file, folders::git_add_files, folders::git_list_all_branches, + folders::git_list_remotes, + folders::git_fetch_remote, + folders::git_add_remote, + folders::git_remove_remote, + folders::git_set_remote_url, folders::git_merge, folders::git_rebase, folders::git_delete_branch, diff --git a/src/components/layout/branch-dropdown.tsx b/src/components/layout/branch-dropdown.tsx index 1851e37..1b84850 100644 --- a/src/components/layout/branch-dropdown.tsx +++ b/src/components/layout/branch-dropdown.tsx @@ -21,6 +21,7 @@ import { FolderGit2, FolderOpen, ArrowLeftRight, + Globe, } from "lucide-react" import { DropdownMenu, @@ -80,6 +81,7 @@ import { openCommitWindow, setFolderParentBranch, } from "@/lib/tauri" +import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog" import { disposeTauriListener } from "@/lib/tauri-listener" import type { GitBranchList } from "@/lib/types" import { toast } from "sonner" @@ -131,11 +133,24 @@ export function BranchDropdown({ const [worktreeOpen, setWorktreeOpen] = useState(false) const [worktreeBranchName, setWorktreeBranchName] = useState("") const [worktreePath, setWorktreePath] = useState("") + const [manageRemotesOpen, setManageRemotesOpen] = useState(false) const taskSeq = useRef(0) const worktreeBranchSet = useMemo( () => new Set(branchList.worktree_branches), [branchList.worktree_branches] ) + const groupedRemoteBranches = useMemo(() => { + const groups: Record = {} + 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 useEffect(() => { if (!folder) return @@ -611,6 +626,19 @@ export function BranchDropdown({ + + { + setDropdownOpen(false) + setManageRemotesOpen(true) + }} + > + + {t("manageRemotes")} + + + {branchLoading ? (
@@ -643,6 +671,20 @@ export function BranchDropdown({ {t("noRemoteBranches")} + ) : hasMultipleRemotes ? ( + remoteNames.map((remoteName) => ( + + + + {remoteName} ({groupedRemoteBranches[remoteName].length}) + + + {groupedRemoteBranches[remoteName].map((b) => + renderBranchItem(b, true) + )} + + + )) ) : ( branchList.remote.map((b) => renderBranchItem(b, true)) )} @@ -782,6 +824,13 @@ export function BranchDropdown({ + + loadAllBranches()} + /> ) } diff --git a/src/components/layout/remote-manage-dialog.tsx b/src/components/layout/remote-manage-dialog.tsx new file mode 100644 index 0000000..3fdaa59 --- /dev/null +++ b/src/components/layout/remote-manage-dialog.tsx @@ -0,0 +1,250 @@ +"use client" + +import { useState, useEffect } from "react" +import { useTranslations } from "next-intl" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Plus, Trash2 } from "lucide-react" +import { + gitListRemotes, + gitFetchRemote, + gitAddRemote, + gitRemoveRemote, + gitSetRemoteUrl, +} from "@/lib/tauri" + +interface RemoteDraft { + originalName: string | null + originalUrl: string + name: string + url: string + deleted: boolean +} + +interface RemoteManageDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + folderPath: string + onSaved: () => void +} + +export function RemoteManageDialog({ + open, + onOpenChange, + folderPath, + onSaved, +}: RemoteManageDialogProps) { + const t = useTranslations("Folder.branchDropdown.dialogs") + const tCommon = useTranslations("Folder.common") + const [drafts, setDrafts] = useState([]) + const [saving, setSaving] = useState(false) + const [loadingRemotes, setLoadingRemotes] = useState(false) + const [errors, setErrors] = useState>({}) + + useEffect(() => { + if (open) { + setErrors({}) + setLoadingRemotes(true) + gitListRemotes(folderPath) + .then((remotes) => { + setDrafts( + remotes.map((r) => ({ + originalName: r.name, + originalUrl: r.url, + name: r.name, + url: r.url, + deleted: false, + })) + ) + }) + .catch((err) => { + console.error("Failed to load remotes:", err) + setDrafts([]) + }) + .finally(() => setLoadingRemotes(false)) + } + }, [open, folderPath]) + + const addDraft = () => { + setDrafts((prev) => [ + ...prev, + { originalName: null, originalUrl: "", name: "", url: "", deleted: false }, + ]) + } + + const updateDraft = ( + index: number, + field: "name" | "url", + value: string + ) => { + setDrafts((prev) => + prev.map((d, i) => (i === index ? { ...d, [field]: value } : d)) + ) + } + + const removeDraft = (index: number) => { + setDrafts((prev) => + prev.map((d, i) => (i === index ? { ...d, deleted: true } : d)) + ) + } + + const extractError = (err: unknown): string => { + if (err && typeof err === "object") { + const e = err as Record + if (typeof e.detail === "string" && e.detail) return e.detail + if (typeof e.message === "string" && e.message) return e.message + } + return String(err) + } + + const handleSave = async () => { + setSaving(true) + setErrors({}) + const newErrors: Record = {} + try { + // Process deletions first + for (const draft of drafts) { + if (draft.deleted && draft.originalName != null) { + await gitRemoveRemote(folderPath, draft.originalName) + } + } + // Process additions + for (let i = 0; i < drafts.length; i++) { + const draft = drafts[i] + if (draft.deleted) continue + if (draft.originalName == null && draft.name && draft.url) { + try { + await gitAddRemote(folderPath, draft.name, draft.url) + } catch (err) { + newErrors[i] = extractError(err) + } + } + } + // Process URL modifications + for (let i = 0; i < drafts.length; i++) { + const draft = drafts[i] + if (draft.deleted || draft.originalName == null) continue + if (draft.url !== draft.originalUrl) { + try { + await gitSetRemoteUrl(folderPath, draft.originalName, draft.url) + } catch (err) { + newErrors[i] = extractError(err) + } + } + } + // Fetch all surviving remotes + for (let i = 0; i < drafts.length; i++) { + const draft = drafts[i] + if (draft.deleted || newErrors[i]) continue + const remoteName = draft.originalName ?? draft.name + if (!remoteName) continue + try { + await gitFetchRemote(folderPath, remoteName) + } catch (err) { + newErrors[i] = extractError(err) + } + } + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors) + } else { + onSaved() + onOpenChange(false) + } + } catch (err) { + console.error("Failed to save remotes:", err) + } finally { + setSaving(false) + } + } + + const visibleDrafts = drafts.filter((d) => !d.deleted) + + return ( + + + + {t("manageRemotesTitle")} + + +
+ {loadingRemotes ? ( +

+ ... +

+ ) : visibleDrafts.length === 0 ? ( +

+ {t("manageRemotesEmpty")} +

+ ) : ( + drafts.map( + (draft, index) => + !draft.deleted && ( +
+
+ + updateDraft(index, "name", e.target.value) + } + disabled={draft.originalName != null} + className={`h-8 text-sm w-32 shrink-0 ${errors[index] ? "border-destructive" : ""}`} + /> + + updateDraft(index, "url", e.target.value) + } + className={`h-8 text-sm font-mono flex-1 ${errors[index] ? "border-destructive" : ""}`} + /> + +
+ {errors[index] && ( +

+ {errors[index]} +

+ )} +
+ ) + ) + )} +
+
+ + +
+ + +
+
+
+
+ ) +} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index fb651cb..d8d72fa 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -794,6 +794,7 @@ "newWorktree": "Worktree جديد...", "stashChanges": "تخزين التغييرات في stash", "stashPop": "استرجاع stash...", + "manageRemotes": "إدارة المستودعات البعيدة...", "localBranches": "الفروع المحلية ({count, plural, one {#} other {#}})", "noLocalBranches": "لا توجد فروع محلية", "remoteBranches": "الفروع البعيدة ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "إنشاء worktree جديد من الفرع الحالي {branch}", "branchNameLabel": "اسم الفرع", "worktreePathLabel": "مسار worktree", - "worktreePathPlaceholder": "مسار worktree" + "worktreePathPlaceholder": "مسار worktree", + "manageRemotesTitle": "إدارة المستودعات البعيدة", + "manageRemotesEmpty": "لم يتم تكوين أي مستودعات بعيدة", + "remoteNamePlaceholder": "اسم المستودع البعيد", + "remoteUrlPlaceholder": "عنوان URL للمستودع البعيد", + "addRemote": "إضافة", + "savingRemotes": "جارٍ الحفظ..." } }, "commitDialog": { diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 4402d85..59cc818 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -794,6 +794,7 @@ "newWorktree": "Neuer Worktree...", "stashChanges": "Änderungen stashen", "stashPop": "Stash anwenden...", + "manageRemotes": "Remotes verwalten...", "localBranches": "Lokale Branches ({count, plural, one {#} other {#}})", "noLocalBranches": "Keine lokalen Branches", "remoteBranches": "Remote-Branches ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "Neuen Worktree vom aktuellen Branch {branch} erstellen", "branchNameLabel": "Branch-Name", "worktreePathLabel": "Worktree-Pfad", - "worktreePathPlaceholder": "Worktree-Pfad" + "worktreePathPlaceholder": "Worktree-Pfad", + "manageRemotesTitle": "Remotes verwalten", + "manageRemotesEmpty": "Keine Remotes konfiguriert", + "remoteNamePlaceholder": "Remote-Name", + "remoteUrlPlaceholder": "Remote-URL", + "addRemote": "Hinzufügen", + "savingRemotes": "Speichern..." } }, "commitDialog": { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index f1e80c1..891297e 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -794,6 +794,7 @@ "newWorktree": "New worktree...", "stashChanges": "Stash changes", "stashPop": "Pop stash...", + "manageRemotes": "Manage Remotes...", "localBranches": "Local branches ({count, plural, one {#} other {#}})", "noLocalBranches": "No local branches", "remoteBranches": "Remote branches ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "Create a new worktree from current branch {branch}", "branchNameLabel": "Branch name", "worktreePathLabel": "Worktree path", - "worktreePathPlaceholder": "Worktree path" + "worktreePathPlaceholder": "Worktree path", + "manageRemotesTitle": "Manage Remotes", + "manageRemotesEmpty": "No remotes configured", + "remoteNamePlaceholder": "Remote name", + "remoteUrlPlaceholder": "Remote URL", + "addRemote": "Add", + "savingRemotes": "Saving..." } }, "commitDialog": { diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 96380f9..7ba8977 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -794,6 +794,7 @@ "newWorktree": "Nuevo worktree...", "stashChanges": "Guardar cambios en stash", "stashPop": "Aplicar stash...", + "manageRemotes": "Gestionar remotos...", "localBranches": "Ramas locales ({count, plural, one {#} other {#}})", "noLocalBranches": "Sin ramas locales", "remoteBranches": "Ramas remotas ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "Crear un nuevo worktree desde la rama actual {branch}", "branchNameLabel": "Nombre de la rama", "worktreePathLabel": "Ruta del worktree", - "worktreePathPlaceholder": "Ruta del worktree" + "worktreePathPlaceholder": "Ruta del worktree", + "manageRemotesTitle": "Gestionar remotos", + "manageRemotesEmpty": "No hay remotos configurados", + "remoteNamePlaceholder": "Nombre del remoto", + "remoteUrlPlaceholder": "URL del remoto", + "addRemote": "Añadir", + "savingRemotes": "Guardando..." } }, "commitDialog": { diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 8188c08..8ea9ea3 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -794,6 +794,7 @@ "newWorktree": "Nouveau worktree...", "stashChanges": "Stash des changements", "stashPop": "Appliquer le stash...", + "manageRemotes": "Gérer les dépôts distants...", "localBranches": "Branches locales ({count, plural, one {#} other {#}})", "noLocalBranches": "Aucune branche locale", "remoteBranches": "Branches distantes ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "Créer un nouveau worktree depuis la branche actuelle {branch}", "branchNameLabel": "Nom de la branche", "worktreePathLabel": "Chemin du worktree", - "worktreePathPlaceholder": "Chemin du worktree" + "worktreePathPlaceholder": "Chemin du worktree", + "manageRemotesTitle": "Gérer les dépôts distants", + "manageRemotesEmpty": "Aucun dépôt distant configuré", + "remoteNamePlaceholder": "Nom du dépôt distant", + "remoteUrlPlaceholder": "URL du dépôt distant", + "addRemote": "Ajouter", + "savingRemotes": "Enregistrement..." } }, "commitDialog": { diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 211572c..4da2571 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -794,6 +794,7 @@ "newWorktree": "新規ワークツリー...", "stashChanges": "変更を stash", "stashPop": "stash を pop...", + "manageRemotes": "リモート管理...", "localBranches": "ローカルブランチ ({count, plural, one {#} other {#}})", "noLocalBranches": "ローカルブランチはありません", "remoteBranches": "リモートブランチ ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "現在のブランチ {branch} から新しいワークツリーを作成", "branchNameLabel": "ブランチ名", "worktreePathLabel": "ワークツリーのパス", - "worktreePathPlaceholder": "ワークツリーのパス" + "worktreePathPlaceholder": "ワークツリーのパス", + "manageRemotesTitle": "リモート管理", + "manageRemotesEmpty": "リモートが設定されていません", + "remoteNamePlaceholder": "リモート名", + "remoteUrlPlaceholder": "リモート URL", + "addRemote": "追加", + "savingRemotes": "保存中..." } }, "commitDialog": { diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index b58fa62..3f2876d 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -794,6 +794,7 @@ "newWorktree": "새 워크트리...", "stashChanges": "변경 사항 stash", "stashPop": "stash pop...", + "manageRemotes": "원격 관리...", "localBranches": "로컬 브랜치 ({count, plural, one {#} other {#}})", "noLocalBranches": "로컬 브랜치가 없습니다", "remoteBranches": "원격 브랜치 ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "현재 브랜치 {branch}에서 새 워크트리를 만듭니다", "branchNameLabel": "브랜치 이름", "worktreePathLabel": "워크트리 경로", - "worktreePathPlaceholder": "워크트리 경로" + "worktreePathPlaceholder": "워크트리 경로", + "manageRemotesTitle": "원격 관리", + "manageRemotesEmpty": "구성된 원격이 없습니다", + "remoteNamePlaceholder": "원격 이름", + "remoteUrlPlaceholder": "원격 URL", + "addRemote": "추가", + "savingRemotes": "저장 중..." } }, "commitDialog": { diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 1ce9763..a473c07 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -794,6 +794,7 @@ "newWorktree": "Novo worktree...", "stashChanges": "Fazer stash das alterações", "stashPop": "Aplicar stash...", + "manageRemotes": "Gerenciar remotos...", "localBranches": "Branches locais ({count, plural, one {#} other {#}})", "noLocalBranches": "Sem branches locais", "remoteBranches": "Branches remotas ({count, plural, one {#} other {#}})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "Criar um novo worktree a partir da branch atual {branch}", "branchNameLabel": "Nome da branch", "worktreePathLabel": "Caminho do worktree", - "worktreePathPlaceholder": "Caminho do worktree" + "worktreePathPlaceholder": "Caminho do worktree", + "manageRemotesTitle": "Gerenciar remotos", + "manageRemotesEmpty": "Nenhum remoto configurado", + "remoteNamePlaceholder": "Nome do remoto", + "remoteUrlPlaceholder": "URL do remoto", + "addRemote": "Adicionar", + "savingRemotes": "Salvando..." } }, "commitDialog": { diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 88afb2c..8ce10bf 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -794,6 +794,7 @@ "newWorktree": "新建工作树...", "stashChanges": "贮藏更改", "stashPop": "取消贮藏...", + "manageRemotes": "管理远程...", "localBranches": "本地分支 ({count})", "noLocalBranches": "无本地分支", "remoteBranches": "远程分支 ({count})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "从当前分支 {branch} 创建新的工作树", "branchNameLabel": "分支名称", "worktreePathLabel": "工作树路径", - "worktreePathPlaceholder": "工作树路径" + "worktreePathPlaceholder": "工作树路径", + "manageRemotesTitle": "管理远程", + "manageRemotesEmpty": "未配置远程仓库", + "remoteNamePlaceholder": "远程名称", + "remoteUrlPlaceholder": "远程 URL", + "addRemote": "添加", + "savingRemotes": "保存中..." } }, "commitDialog": { diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index f29dcb6..ad716b1 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -794,6 +794,7 @@ "newWorktree": "新增工作樹...", "stashChanges": "暫存變更", "stashPop": "取消暫存...", + "manageRemotes": "管理遠端...", "localBranches": "本地分支 ({count})", "noLocalBranches": "無本地分支", "remoteBranches": "遠端分支 ({count})", @@ -807,7 +808,13 @@ "newWorktreeDescription": "從目前分支 {branch} 建立新的工作樹", "branchNameLabel": "分支名稱", "worktreePathLabel": "工作樹路徑", - "worktreePathPlaceholder": "工作樹路徑" + "worktreePathPlaceholder": "工作樹路徑", + "manageRemotesTitle": "管理遠端", + "manageRemotesEmpty": "未設定遠端儲存庫", + "remoteNamePlaceholder": "遠端名稱", + "remoteUrlPlaceholder": "遠端 URL", + "addRemote": "新增", + "savingRemotes": "儲存中..." } }, "commitDialog": { diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 65da073..30710fb 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -25,6 +25,7 @@ import type { GitPushResult, GitMergeResult, GitCommitResult, + GitRemote, PreflightResult, FolderCommand, TerminalInfo, @@ -522,6 +523,40 @@ export async function gitStashPop(path: string): Promise { return invoke("git_stash_pop", { path }) } +export async function gitListRemotes(path: string): Promise { + return invoke("git_list_remotes", { path }) +} + +export async function gitFetchRemote( + path: string, + name: string +): Promise { + return invoke("git_fetch_remote", { path, name }) +} + +export async function gitAddRemote( + path: string, + name: string, + url: string +): Promise { + return invoke("git_add_remote", { path, name, url }) +} + +export async function gitRemoveRemote( + path: string, + name: string +): Promise { + return invoke("git_remove_remote", { path, name }) +} + +export async function gitSetRemoteUrl( + path: string, + name: string, + url: string +): Promise { + return invoke("git_set_remote_url", { path, name, url }) +} + export async function gitStatus(path: string): Promise { return invoke("git_status", { path }) } diff --git a/src/lib/types.ts b/src/lib/types.ts index dd54b57..b70cede 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -683,6 +683,11 @@ export interface GitCommitResult { committed_files: number } +export interface GitRemote { + name: string + url: string +} + export type FileTreeNode = | { kind: "file"; name: string; path: string } | { kind: "dir"; name: string; path: string; children: FileTreeNode[] }