初步支持远程Git管理

This commit is contained in:
xintaofei
2026-03-13 22:55:55 +08:00
parent 0720aa930b
commit 767d43b0cf
16 changed files with 535 additions and 10 deletions

View File

@@ -54,6 +54,12 @@ pub struct GitCommitResult {
pub committed_files: usize, pub committed_files: usize,
} }
#[derive(Debug, Serialize)]
pub struct GitRemote {
pub name: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
struct GitCommitSucceededEvent { struct GitCommitSucceededEvent {
folder_id: i32, folder_id: i32,
@@ -1100,6 +1106,111 @@ pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCom
}) })
} }
#[tauri::command]
pub async fn git_list_remotes(path: String) -> Result<Vec<GitRemote>, 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<String, AppCommandError> {
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] #[tauri::command]
pub async fn git_merge( pub async fn git_merge(
path: String, path: String,

View File

@@ -199,6 +199,11 @@ pub fn run() {
folders::git_rollback_file, folders::git_rollback_file,
folders::git_add_files, folders::git_add_files,
folders::git_list_all_branches, 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_merge,
folders::git_rebase, folders::git_rebase,
folders::git_delete_branch, folders::git_delete_branch,

View File

@@ -21,6 +21,7 @@ import {
FolderGit2, FolderGit2,
FolderOpen, FolderOpen,
ArrowLeftRight, ArrowLeftRight,
Globe,
} from "lucide-react" } from "lucide-react"
import { import {
DropdownMenu, DropdownMenu,
@@ -80,6 +81,7 @@ import {
openCommitWindow, openCommitWindow,
setFolderParentBranch, setFolderParentBranch,
} from "@/lib/tauri" } from "@/lib/tauri"
import { RemoteManageDialog } from "@/components/layout/remote-manage-dialog"
import { disposeTauriListener } from "@/lib/tauri-listener" import { disposeTauriListener } from "@/lib/tauri-listener"
import type { GitBranchList } from "@/lib/types" import type { GitBranchList } from "@/lib/types"
import { toast } from "sonner" import { toast } from "sonner"
@@ -131,11 +133,24 @@ export function BranchDropdown({
const [worktreeOpen, setWorktreeOpen] = useState(false) const [worktreeOpen, setWorktreeOpen] = useState(false)
const [worktreeBranchName, setWorktreeBranchName] = useState("") const [worktreeBranchName, setWorktreeBranchName] = useState("")
const [worktreePath, setWorktreePath] = useState("") const [worktreePath, setWorktreePath] = useState("")
const [manageRemotesOpen, setManageRemotesOpen] = useState(false)
const taskSeq = useRef(0) const taskSeq = useRef(0)
const worktreeBranchSet = useMemo( const worktreeBranchSet = useMemo(
() => new Set(branchList.worktree_branches), () => new Set(branchList.worktree_branches),
[branchList.worktree_branches] [branchList.worktree_branches]
) )
const groupedRemoteBranches = useMemo(() => {
const groups: Record<string, string[]> = {}
for (const b of branchList.remote) {
const slashIndex = b.indexOf("/")
const remoteName = slashIndex > 0 ? b.substring(0, slashIndex) : "origin"
if (!groups[remoteName]) groups[remoteName] = []
groups[remoteName].push(b)
}
return groups
}, [branchList.remote])
const remoteNames = Object.keys(groupedRemoteBranches)
const hasMultipleRemotes = remoteNames.length > 1
useEffect(() => { useEffect(() => {
if (!folder) return if (!folder) return
@@ -611,6 +626,19 @@ export function BranchDropdown({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
disabled={loading}
onSelect={() => {
setDropdownOpen(false)
setManageRemotesOpen(true)
}}
>
<Globe className="h-3.5 w-3.5" />
{t("manageRemotes")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
{branchLoading ? ( {branchLoading ? (
<div className="flex items-center justify-center py-3"> <div className="flex items-center justify-center py-3">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
@@ -643,6 +671,20 @@ export function BranchDropdown({
<DropdownMenuItem disabled> <DropdownMenuItem disabled>
{t("noRemoteBranches")} {t("noRemoteBranches")}
</DropdownMenuItem> </DropdownMenuItem>
) : hasMultipleRemotes ? (
remoteNames.map((remoteName) => (
<Collapsible key={remoteName}>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 pl-6 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{remoteName} ({groupedRemoteBranches[remoteName].length})
</CollapsibleTrigger>
<CollapsibleContent>
{groupedRemoteBranches[remoteName].map((b) =>
renderBranchItem(b, true)
)}
</CollapsibleContent>
</Collapsible>
))
) : ( ) : (
branchList.remote.map((b) => renderBranchItem(b, true)) branchList.remote.map((b) => renderBranchItem(b, true))
)} )}
@@ -782,6 +824,13 @@ export function BranchDropdown({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<RemoteManageDialog
open={manageRemotesOpen}
onOpenChange={setManageRemotesOpen}
folderPath={folderPath}
onSaved={() => loadAllBranches()}
/>
</> </>
) )
} }

View File

@@ -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<RemoteDraft[]>([])
const [saving, setSaving] = useState(false)
const [loadingRemotes, setLoadingRemotes] = useState(false)
const [errors, setErrors] = useState<Record<number, string>>({})
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<string, unknown>
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<number, string> = {}
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{t("manageRemotesTitle")}</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-72">
<div className="space-y-2 pr-2">
{loadingRemotes ? (
<p className="text-sm text-muted-foreground text-center py-4">
...
</p>
) : visibleDrafts.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">
{t("manageRemotesEmpty")}
</p>
) : (
drafts.map(
(draft, index) =>
!draft.deleted && (
<div key={index} className="space-y-1">
<div className="flex items-center gap-2">
<Input
placeholder={t("remoteNamePlaceholder")}
value={draft.name}
onChange={(e) =>
updateDraft(index, "name", e.target.value)
}
disabled={draft.originalName != null}
className={`h-8 text-sm w-32 shrink-0 ${errors[index] ? "border-destructive" : ""}`}
/>
<Input
placeholder={t("remoteUrlPlaceholder")}
value={draft.url}
onChange={(e) =>
updateDraft(index, "url", e.target.value)
}
className={`h-8 text-sm font-mono flex-1 ${errors[index] ? "border-destructive" : ""}`}
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => removeDraft(index)}
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
{errors[index] && (
<p className="text-xs text-destructive pl-1 truncate">
{errors[index]}
</p>
)}
</div>
)
)
)}
</div>
</ScrollArea>
<DialogFooter className="flex items-center justify-between sm:justify-between">
<Button variant="outline" size="sm" onClick={addDraft}>
<Plus className="h-3.5 w-3.5 mr-1" />
{t("addRemote")}
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onOpenChange(false)}
>
{tCommon("cancel")}
</Button>
<Button size="sm" onClick={handleSave} disabled={saving}>
{saving ? t("savingRemotes") : tCommon("save")}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -794,6 +794,7 @@
"newWorktree": "Worktree جديد...", "newWorktree": "Worktree جديد...",
"stashChanges": "تخزين التغييرات في stash", "stashChanges": "تخزين التغييرات في stash",
"stashPop": "استرجاع stash...", "stashPop": "استرجاع stash...",
"manageRemotes": "إدارة المستودعات البعيدة...",
"localBranches": "الفروع المحلية ({count, plural, one {#} other {#}})", "localBranches": "الفروع المحلية ({count, plural, one {#} other {#}})",
"noLocalBranches": "لا توجد فروع محلية", "noLocalBranches": "لا توجد فروع محلية",
"remoteBranches": "الفروع البعيدة ({count, plural, one {#} other {#}})", "remoteBranches": "الفروع البعيدة ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "إنشاء worktree جديد من الفرع الحالي {branch}", "newWorktreeDescription": "إنشاء worktree جديد من الفرع الحالي {branch}",
"branchNameLabel": "اسم الفرع", "branchNameLabel": "اسم الفرع",
"worktreePathLabel": "مسار worktree", "worktreePathLabel": "مسار worktree",
"worktreePathPlaceholder": "مسار worktree" "worktreePathPlaceholder": "مسار worktree",
"manageRemotesTitle": "إدارة المستودعات البعيدة",
"manageRemotesEmpty": "لم يتم تكوين أي مستودعات بعيدة",
"remoteNamePlaceholder": "اسم المستودع البعيد",
"remoteUrlPlaceholder": "عنوان URL للمستودع البعيد",
"addRemote": "إضافة",
"savingRemotes": "جارٍ الحفظ..."
} }
}, },
"commitDialog": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "Neuer Worktree...", "newWorktree": "Neuer Worktree...",
"stashChanges": "Änderungen stashen", "stashChanges": "Änderungen stashen",
"stashPop": "Stash anwenden...", "stashPop": "Stash anwenden...",
"manageRemotes": "Remotes verwalten...",
"localBranches": "Lokale Branches ({count, plural, one {#} other {#}})", "localBranches": "Lokale Branches ({count, plural, one {#} other {#}})",
"noLocalBranches": "Keine lokalen Branches", "noLocalBranches": "Keine lokalen Branches",
"remoteBranches": "Remote-Branches ({count, plural, one {#} other {#}})", "remoteBranches": "Remote-Branches ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "Neuen Worktree vom aktuellen Branch {branch} erstellen", "newWorktreeDescription": "Neuen Worktree vom aktuellen Branch {branch} erstellen",
"branchNameLabel": "Branch-Name", "branchNameLabel": "Branch-Name",
"worktreePathLabel": "Worktree-Pfad", "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": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "New worktree...", "newWorktree": "New worktree...",
"stashChanges": "Stash changes", "stashChanges": "Stash changes",
"stashPop": "Pop stash...", "stashPop": "Pop stash...",
"manageRemotes": "Manage Remotes...",
"localBranches": "Local branches ({count, plural, one {#} other {#}})", "localBranches": "Local branches ({count, plural, one {#} other {#}})",
"noLocalBranches": "No local branches", "noLocalBranches": "No local branches",
"remoteBranches": "Remote branches ({count, plural, one {#} other {#}})", "remoteBranches": "Remote branches ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "Create a new worktree from current branch {branch}", "newWorktreeDescription": "Create a new worktree from current branch {branch}",
"branchNameLabel": "Branch name", "branchNameLabel": "Branch name",
"worktreePathLabel": "Worktree path", "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": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "Nuevo worktree...", "newWorktree": "Nuevo worktree...",
"stashChanges": "Guardar cambios en stash", "stashChanges": "Guardar cambios en stash",
"stashPop": "Aplicar stash...", "stashPop": "Aplicar stash...",
"manageRemotes": "Gestionar remotos...",
"localBranches": "Ramas locales ({count, plural, one {#} other {#}})", "localBranches": "Ramas locales ({count, plural, one {#} other {#}})",
"noLocalBranches": "Sin ramas locales", "noLocalBranches": "Sin ramas locales",
"remoteBranches": "Ramas remotas ({count, plural, one {#} other {#}})", "remoteBranches": "Ramas remotas ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "Crear un nuevo worktree desde la rama actual {branch}", "newWorktreeDescription": "Crear un nuevo worktree desde la rama actual {branch}",
"branchNameLabel": "Nombre de la rama", "branchNameLabel": "Nombre de la rama",
"worktreePathLabel": "Ruta del worktree", "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": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "Nouveau worktree...", "newWorktree": "Nouveau worktree...",
"stashChanges": "Stash des changements", "stashChanges": "Stash des changements",
"stashPop": "Appliquer le stash...", "stashPop": "Appliquer le stash...",
"manageRemotes": "Gérer les dépôts distants...",
"localBranches": "Branches locales ({count, plural, one {#} other {#}})", "localBranches": "Branches locales ({count, plural, one {#} other {#}})",
"noLocalBranches": "Aucune branche locale", "noLocalBranches": "Aucune branche locale",
"remoteBranches": "Branches distantes ({count, plural, one {#} other {#}})", "remoteBranches": "Branches distantes ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "Créer un nouveau worktree depuis la branche actuelle {branch}", "newWorktreeDescription": "Créer un nouveau worktree depuis la branche actuelle {branch}",
"branchNameLabel": "Nom de la branche", "branchNameLabel": "Nom de la branche",
"worktreePathLabel": "Chemin du worktree", "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": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "新規ワークツリー...", "newWorktree": "新規ワークツリー...",
"stashChanges": "変更を stash", "stashChanges": "変更を stash",
"stashPop": "stash を pop...", "stashPop": "stash を pop...",
"manageRemotes": "リモート管理...",
"localBranches": "ローカルブランチ ({count, plural, one {#} other {#}})", "localBranches": "ローカルブランチ ({count, plural, one {#} other {#}})",
"noLocalBranches": "ローカルブランチはありません", "noLocalBranches": "ローカルブランチはありません",
"remoteBranches": "リモートブランチ ({count, plural, one {#} other {#}})", "remoteBranches": "リモートブランチ ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "現在のブランチ {branch} から新しいワークツリーを作成", "newWorktreeDescription": "現在のブランチ {branch} から新しいワークツリーを作成",
"branchNameLabel": "ブランチ名", "branchNameLabel": "ブランチ名",
"worktreePathLabel": "ワークツリーのパス", "worktreePathLabel": "ワークツリーのパス",
"worktreePathPlaceholder": "ワークツリーのパス" "worktreePathPlaceholder": "ワークツリーのパス",
"manageRemotesTitle": "リモート管理",
"manageRemotesEmpty": "リモートが設定されていません",
"remoteNamePlaceholder": "リモート名",
"remoteUrlPlaceholder": "リモート URL",
"addRemote": "追加",
"savingRemotes": "保存中..."
} }
}, },
"commitDialog": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "새 워크트리...", "newWorktree": "새 워크트리...",
"stashChanges": "변경 사항 stash", "stashChanges": "변경 사항 stash",
"stashPop": "stash pop...", "stashPop": "stash pop...",
"manageRemotes": "원격 관리...",
"localBranches": "로컬 브랜치 ({count, plural, one {#} other {#}})", "localBranches": "로컬 브랜치 ({count, plural, one {#} other {#}})",
"noLocalBranches": "로컬 브랜치가 없습니다", "noLocalBranches": "로컬 브랜치가 없습니다",
"remoteBranches": "원격 브랜치 ({count, plural, one {#} other {#}})", "remoteBranches": "원격 브랜치 ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "현재 브랜치 {branch}에서 새 워크트리를 만듭니다", "newWorktreeDescription": "현재 브랜치 {branch}에서 새 워크트리를 만듭니다",
"branchNameLabel": "브랜치 이름", "branchNameLabel": "브랜치 이름",
"worktreePathLabel": "워크트리 경로", "worktreePathLabel": "워크트리 경로",
"worktreePathPlaceholder": "워크트리 경로" "worktreePathPlaceholder": "워크트리 경로",
"manageRemotesTitle": "원격 관리",
"manageRemotesEmpty": "구성된 원격이 없습니다",
"remoteNamePlaceholder": "원격 이름",
"remoteUrlPlaceholder": "원격 URL",
"addRemote": "추가",
"savingRemotes": "저장 중..."
} }
}, },
"commitDialog": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "Novo worktree...", "newWorktree": "Novo worktree...",
"stashChanges": "Fazer stash das alterações", "stashChanges": "Fazer stash das alterações",
"stashPop": "Aplicar stash...", "stashPop": "Aplicar stash...",
"manageRemotes": "Gerenciar remotos...",
"localBranches": "Branches locais ({count, plural, one {#} other {#}})", "localBranches": "Branches locais ({count, plural, one {#} other {#}})",
"noLocalBranches": "Sem branches locais", "noLocalBranches": "Sem branches locais",
"remoteBranches": "Branches remotas ({count, plural, one {#} other {#}})", "remoteBranches": "Branches remotas ({count, plural, one {#} other {#}})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "Criar um novo worktree a partir da branch atual {branch}", "newWorktreeDescription": "Criar um novo worktree a partir da branch atual {branch}",
"branchNameLabel": "Nome da branch", "branchNameLabel": "Nome da branch",
"worktreePathLabel": "Caminho do worktree", "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": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "新建工作树...", "newWorktree": "新建工作树...",
"stashChanges": "贮藏更改", "stashChanges": "贮藏更改",
"stashPop": "取消贮藏...", "stashPop": "取消贮藏...",
"manageRemotes": "管理远程...",
"localBranches": "本地分支 ({count})", "localBranches": "本地分支 ({count})",
"noLocalBranches": "无本地分支", "noLocalBranches": "无本地分支",
"remoteBranches": "远程分支 ({count})", "remoteBranches": "远程分支 ({count})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "从当前分支 {branch} 创建新的工作树", "newWorktreeDescription": "从当前分支 {branch} 创建新的工作树",
"branchNameLabel": "分支名称", "branchNameLabel": "分支名称",
"worktreePathLabel": "工作树路径", "worktreePathLabel": "工作树路径",
"worktreePathPlaceholder": "工作树路径" "worktreePathPlaceholder": "工作树路径",
"manageRemotesTitle": "管理远程",
"manageRemotesEmpty": "未配置远程仓库",
"remoteNamePlaceholder": "远程名称",
"remoteUrlPlaceholder": "远程 URL",
"addRemote": "添加",
"savingRemotes": "保存中..."
} }
}, },
"commitDialog": { "commitDialog": {

View File

@@ -794,6 +794,7 @@
"newWorktree": "新增工作樹...", "newWorktree": "新增工作樹...",
"stashChanges": "暫存變更", "stashChanges": "暫存變更",
"stashPop": "取消暫存...", "stashPop": "取消暫存...",
"manageRemotes": "管理遠端...",
"localBranches": "本地分支 ({count})", "localBranches": "本地分支 ({count})",
"noLocalBranches": "無本地分支", "noLocalBranches": "無本地分支",
"remoteBranches": "遠端分支 ({count})", "remoteBranches": "遠端分支 ({count})",
@@ -807,7 +808,13 @@
"newWorktreeDescription": "從目前分支 {branch} 建立新的工作樹", "newWorktreeDescription": "從目前分支 {branch} 建立新的工作樹",
"branchNameLabel": "分支名稱", "branchNameLabel": "分支名稱",
"worktreePathLabel": "工作樹路徑", "worktreePathLabel": "工作樹路徑",
"worktreePathPlaceholder": "工作樹路徑" "worktreePathPlaceholder": "工作樹路徑",
"manageRemotesTitle": "管理遠端",
"manageRemotesEmpty": "未設定遠端儲存庫",
"remoteNamePlaceholder": "遠端名稱",
"remoteUrlPlaceholder": "遠端 URL",
"addRemote": "新增",
"savingRemotes": "儲存中..."
} }
}, },
"commitDialog": { "commitDialog": {

View File

@@ -25,6 +25,7 @@ import type {
GitPushResult, GitPushResult,
GitMergeResult, GitMergeResult,
GitCommitResult, GitCommitResult,
GitRemote,
PreflightResult, PreflightResult,
FolderCommand, FolderCommand,
TerminalInfo, TerminalInfo,
@@ -522,6 +523,40 @@ export async function gitStashPop(path: string): Promise<string> {
return invoke("git_stash_pop", { path }) return invoke("git_stash_pop", { path })
} }
export async function gitListRemotes(path: string): Promise<GitRemote[]> {
return invoke("git_list_remotes", { path })
}
export async function gitFetchRemote(
path: string,
name: string
): Promise<string> {
return invoke("git_fetch_remote", { path, name })
}
export async function gitAddRemote(
path: string,
name: string,
url: string
): Promise<void> {
return invoke("git_add_remote", { path, name, url })
}
export async function gitRemoveRemote(
path: string,
name: string
): Promise<void> {
return invoke("git_remove_remote", { path, name })
}
export async function gitSetRemoteUrl(
path: string,
name: string,
url: string
): Promise<void> {
return invoke("git_set_remote_url", { path, name, url })
}
export async function gitStatus(path: string): Promise<GitStatusEntry[]> { export async function gitStatus(path: string): Promise<GitStatusEntry[]> {
return invoke("git_status", { path }) return invoke("git_status", { path })
} }

View File

@@ -683,6 +683,11 @@ export interface GitCommitResult {
committed_files: number committed_files: number
} }
export interface GitRemote {
name: string
url: string
}
export type FileTreeNode = export type FileTreeNode =
| { kind: "file"; name: string; path: string } | { kind: "file"; name: string; path: string }
| { kind: "dir"; name: string; path: string; children: FileTreeNode[] } | { kind: "dir"; name: string; path: string; children: FileTreeNode[] }