diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index 33a04d9..638a4c6 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -2055,6 +2055,45 @@ pub async fn git_delete_branch( Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } +pub(crate) async fn git_delete_remote_branch_core( + path: &str, + remote: &str, + branch: &str, + credentials: Option<&GitCredentials>, + db: &AppDatabase, + data_dir: &std::path::Path, +) -> Result { + let mut cmd = crate::process::tokio_command("git"); + cmd.args(["push", remote, "--delete", branch]) + .current_dir(path); + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(remote), credentials, db, data_dir) + .await; + + let output = cmd.output().await.map_err(AppCommandError::io)?; + + if !output.status.success() { + return Err(classify_remote_git_error("push --delete", &output.stderr)); + } + Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) +} + +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn git_delete_remote_branch( + path: String, + remote: String, + branch: String, + credentials: Option, + db: tauri::State<'_, AppDatabase>, + app_handle: tauri::AppHandle, +) -> Result { + let data_dir = app_handle.path().app_data_dir().map_err(|e| { + AppCommandError::external_command("Failed to resolve app data dir", e.to_string()) + })?; + git_delete_remote_branch_core(&path, &remote, &branch, credentials.as_ref(), &db, &data_dir) + .await +} + #[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_list_conflicts(path: String) -> Result, AppCommandError> { detect_conflicts(&path).await diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 59e7b17..3ff1a77 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -279,6 +279,7 @@ mod tauri_app { folders::git_merge, folders::git_rebase, folders::git_delete_branch, + folders::git_delete_remote_branch, folders::git_list_conflicts, folders::git_conflict_file_versions, folders::git_resolve_conflict, diff --git a/src-tauri/src/web/handlers/git.rs b/src-tauri/src/web/handlers/git.rs index 2e848a0..fab406b 100644 --- a/src-tauri/src/web/handlers/git.rs +++ b/src-tauri/src/web/handlers/git.rs @@ -451,6 +451,32 @@ pub async fn git_delete_branch( Ok(Json(())) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GitDeleteRemoteBranchParams { + pub path: String, + pub remote: String, + pub branch: String, + pub credentials: Option, +} + +pub async fn git_delete_remote_branch( + Extension(state): Extension>, + Json(params): Json, +) -> Result, AppCommandError> { + let db = &state.db; + folder_commands::git_delete_remote_branch_core( + ¶ms.path, + ¶ms.remote, + ¶ms.branch, + params.credentials.as_ref(), + db, + &state.data_dir, + ) + .await?; + Ok(Json(())) +} + pub async fn git_list_conflicts( Json(params): Json, ) -> Result>, AppCommandError> { diff --git a/src-tauri/src/web/router.rs b/src-tauri/src/web/router.rs index 8828831..8bc2ff3 100644 --- a/src-tauri/src/web/router.rs +++ b/src-tauri/src/web/router.rs @@ -106,6 +106,7 @@ pub fn build_router(state: Arc, token: String, static_dir: std::path:: .route("/git_fetch", post(handlers::git::git_fetch)) .route("/git_commit", post(handlers::git::git_commit)) .route("/git_fetch_remote", post(handlers::git::git_fetch_remote)) + .route("/git_delete_remote_branch", post(handlers::git::git_delete_remote_branch)) .route("/clone_repository", post(handlers::git::clone_repository)) // ─── Files ─── .route("/read_file_preview", post(handlers::files::read_file_preview)) diff --git a/src/components/layout/branch-dropdown.tsx b/src/components/layout/branch-dropdown.tsx index c1f81a3..b9ac6a2 100644 --- a/src/components/layout/branch-dropdown.tsx +++ b/src/components/layout/branch-dropdown.tsx @@ -81,6 +81,7 @@ import { gitMerge, gitRebase, gitDeleteBranch, + gitDeleteRemoteBranch, openFolderWindow, openCommitWindow, setFolderParentBranch, @@ -105,7 +106,7 @@ interface BranchDropdownProps { } type ConfirmAction = { - type: "merge" | "rebase" | "delete" | "forceDelete" + type: "merge" | "rebase" | "delete" | "forceDelete" | "deleteRemote" branchName: string } @@ -424,6 +425,18 @@ export function BranchDropdown({ gitDeleteBranch(folderPath, branchName, true) ) break + case "deleteRemote": { + const idx = branchName.indexOf("/") + const remote = branchName.substring(0, idx) + const rb = branchName.substring(idx + 1) + await runGitTask(t("tasks.deleteRemoteBranch", { branchName }), () => + withCredentialRetry( + (creds) => gitDeleteRemoteBranch(folderPath, remote, rb, creds), + { folderPath } + ) + ) + break + } } } @@ -438,6 +451,8 @@ export function BranchDropdown({ return t("confirm.deleteTitle") case "forceDelete": return t("confirm.forceDeleteTitle") + case "deleteRemote": + return t("confirm.deleteRemoteTitle") } } @@ -462,6 +477,10 @@ export function BranchDropdown({ return t("confirm.forceDeleteDescription", { branchName: confirmAction.branchName, }) + case "deleteRemote": + return t("confirm.deleteRemoteDescription", { + branchName: confirmAction.branchName, + }) } } @@ -472,6 +491,8 @@ export function BranchDropdown({ ) { const label = displayName ?? b const isCurrent = b === branch + const isTrackingCurrent = + isRemote && !!branch && b.replace(/^[^/]+\//, "") === branch const isWorktree = worktreeBranchSet.has( isRemote ? b.replace(/^[^/]+\//, "") : b ) @@ -553,14 +574,17 @@ export function BranchDropdown({ branchName: b, })} - {!isRemote && ( + {!isTrackingCurrent && ( <> { setDropdownOpen(false) - setConfirmAction({ type: "delete", branchName: b }) + setConfirmAction({ + type: isRemote ? "deleteRemote" : "delete", + branchName: b, + }) }} > @@ -841,7 +865,11 @@ export function BranchDropdown({ {tCommon("cancel")} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 152eb51..1728739 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -914,6 +914,7 @@ "mergeBranch": "دمج {branchName}", "rebaseTo": "Rebase إلى {branchName}", "deleteBranch": "حذف الفرع {branchName}", + "deleteRemoteBranch": "حذف الفرع البعيد {branchName}", "initGitRepo": "تهيئة مستودع Git", "pullCode": "سحب الكود", "fetchInfo": "جلب المعلومات", @@ -929,7 +930,9 @@ "rebaseDescription": "إجراء rebase للفرع الحالي {currentBranch} على {branchName}؟", "deleteDescription": "حذف الفرع {branchName}؟ لا يمكن التراجع عن هذا الإجراء.", "forceDeleteTitle": "حذف الفرع بالقوة", - "forceDeleteDescription": "الفرع {branchName} لم يتم دمجه بالكامل. هل أنت متأكد من أنك تريد حذفه بالقوة؟ لا يمكن التراجع عن هذا الإجراء." + "forceDeleteDescription": "الفرع {branchName} لم يتم دمجه بالكامل. هل أنت متأكد من أنك تريد حذفه بالقوة؟ لا يمكن التراجع عن هذا الإجراء.", + "deleteRemoteTitle": "حذف الفرع البعيد", + "deleteRemoteDescription": "هل تريد حذف الفرع البعيد {branchName}؟ سيؤدي ذلك إلى إزالته من المستودع البعيد ولا يمكن التراجع عن هذا الإجراء." }, "current": "الحالي", "switchToBranch": "التبديل إلى هذا الفرع", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index d44c854..35286d1 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -914,6 +914,7 @@ "mergeBranch": "{branchName} mergen", "rebaseTo": "Auf {branchName} rebasen", "deleteBranch": "Branch {branchName} löschen", + "deleteRemoteBranch": "Remote-Branch {branchName} löschen", "initGitRepo": "Git-Repository initialisieren", "pullCode": "Code pullen", "fetchInfo": "Informationen fetchen", @@ -929,7 +930,9 @@ "rebaseDescription": "Aktuellen Branch {currentBranch} auf {branchName} rebasen?", "deleteDescription": "Branch {branchName} löschen? Diese Aktion kann nicht rückgängig gemacht werden.", "forceDeleteTitle": "Branch erzwungen löschen", - "forceDeleteDescription": "Der Branch {branchName} ist nicht vollständig gemergt. Möchten Sie ihn wirklich erzwungen löschen? Diese Aktion kann nicht rückgängig gemacht werden." + "forceDeleteDescription": "Der Branch {branchName} ist nicht vollständig gemergt. Möchten Sie ihn wirklich erzwungen löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteRemoteTitle": "Remote-Branch löschen", + "deleteRemoteDescription": "Remote-Branch {branchName} löschen? Dies entfernt ihn aus dem Remote-Repository und kann nicht rückgängig gemacht werden." }, "current": "Aktuell", "switchToBranch": "Zu diesem Branch wechseln", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 1dbd71f..9ca7b37 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -914,6 +914,7 @@ "mergeBranch": "Merge {branchName}", "rebaseTo": "Rebase to {branchName}", "deleteBranch": "Delete branch {branchName}", + "deleteRemoteBranch": "Delete remote branch {branchName}", "initGitRepo": "Initialize Git repository", "pullCode": "Pull code", "fetchInfo": "Fetch info", @@ -929,7 +930,9 @@ "rebaseDescription": "Rebase current branch {currentBranch} onto {branchName}?", "deleteDescription": "Delete branch {branchName}? This action cannot be undone.", "forceDeleteTitle": "Force Delete Branch", - "forceDeleteDescription": "Branch {branchName} is not fully merged. Are you sure you want to force delete it? This action cannot be undone." + "forceDeleteDescription": "Branch {branchName} is not fully merged. Are you sure you want to force delete it? This action cannot be undone.", + "deleteRemoteTitle": "Delete Remote Branch", + "deleteRemoteDescription": "Delete remote branch {branchName}? This will remove it from the remote repository and cannot be undone." }, "current": "Current", "switchToBranch": "Switch to this branch", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 349b1f6..9171125 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -914,6 +914,7 @@ "mergeBranch": "Fusionar {branchName}", "rebaseTo": "Rebase a {branchName}", "deleteBranch": "Eliminar rama {branchName}", + "deleteRemoteBranch": "Eliminar rama remota {branchName}", "initGitRepo": "Inicializar repositorio Git", "pullCode": "Hacer pull del código", "fetchInfo": "Obtener información", @@ -929,7 +930,9 @@ "rebaseDescription": "¿Hacer rebase de la rama actual {currentBranch} sobre {branchName}?", "deleteDescription": "¿Eliminar la rama {branchName}? Esta acción no se puede deshacer.", "forceDeleteTitle": "Forzar eliminación de rama", - "forceDeleteDescription": "La rama {branchName} no está completamente fusionada. ¿Estás seguro de que quieres forzar su eliminación? Esta acción no se puede deshacer." + "forceDeleteDescription": "La rama {branchName} no está completamente fusionada. ¿Estás seguro de que quieres forzar su eliminación? Esta acción no se puede deshacer.", + "deleteRemoteTitle": "Eliminar rama remota", + "deleteRemoteDescription": "¿Eliminar la rama remota {branchName}? Esto la eliminará del repositorio remoto y no se puede deshacer." }, "current": "Actual", "switchToBranch": "Cambiar a esta rama", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 7f78fe6..6b1f7ae 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -914,6 +914,7 @@ "mergeBranch": "Fusionner {branchName}", "rebaseTo": "Rebase vers {branchName}", "deleteBranch": "Supprimer la branche {branchName}", + "deleteRemoteBranch": "Supprimer la branche distante {branchName}", "initGitRepo": "Initialiser le dépôt Git", "pullCode": "Pull du code", "fetchInfo": "Récupérer les infos", @@ -929,7 +930,9 @@ "rebaseDescription": "Rebaser la branche actuelle {currentBranch} sur {branchName} ?", "deleteDescription": "Supprimer la branche {branchName} ? Cette action est irréversible.", "forceDeleteTitle": "Forcer la suppression de la branche", - "forceDeleteDescription": "La branche {branchName} n'est pas entièrement fusionnée. Êtes-vous sûr de vouloir la supprimer de force ? Cette action est irréversible." + "forceDeleteDescription": "La branche {branchName} n'est pas entièrement fusionnée. Êtes-vous sûr de vouloir la supprimer de force ? Cette action est irréversible.", + "deleteRemoteTitle": "Supprimer la branche distante", + "deleteRemoteDescription": "Supprimer la branche distante {branchName} ? Cette action la supprimera du dépôt distant et ne pourra pas être annulée." }, "current": "Actuelle", "switchToBranch": "Basculer vers cette branche", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 39533eb..7efc015 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -914,6 +914,7 @@ "mergeBranch": "{branchName} をマージ", "rebaseTo": "{branchName} にリベース", "deleteBranch": "ブランチ {branchName} を削除", + "deleteRemoteBranch": "リモートブランチ {branchName} を削除", "initGitRepo": "Git リポジトリを初期化", "pullCode": "コードをプル", "fetchInfo": "情報をフェッチ", @@ -929,7 +930,9 @@ "rebaseDescription": "現在のブランチ {currentBranch} を {branchName} にリベースしますか?", "deleteDescription": "ブランチ {branchName} を削除しますか?この操作は元に戻せません。", "forceDeleteTitle": "ブランチを強制削除", - "forceDeleteDescription": "ブランチ {branchName} はまだ完全にマージされていません。強制削除してもよろしいですか?この操作は元に戻せません。" + "forceDeleteDescription": "ブランチ {branchName} はまだ完全にマージされていません。強制削除してもよろしいですか?この操作は元に戻せません。", + "deleteRemoteTitle": "リモートブランチの削除", + "deleteRemoteDescription": "リモートブランチ {branchName} を削除しますか?この操作はリモートリポジトリからブランチを削除し、元に戻せません。" }, "current": "現在", "switchToBranch": "このブランチに切り替え", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 14406bd..28d92a6 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -914,6 +914,7 @@ "mergeBranch": "{branchName} 병합", "rebaseTo": "{branchName}로 리베이스", "deleteBranch": "브랜치 {branchName} 삭제", + "deleteRemoteBranch": "원격 브랜치 {branchName} 삭제", "initGitRepo": "Git 저장소 초기화", "pullCode": "코드 pull", "fetchInfo": "정보 fetch", @@ -929,7 +930,9 @@ "rebaseDescription": "현재 브랜치 {currentBranch}를 {branchName} 위로 리베이스할까요?", "deleteDescription": "브랜치 {branchName}을(를) 삭제할까요? 이 작업은 되돌릴 수 없습니다.", "forceDeleteTitle": "브랜치 강제 삭제", - "forceDeleteDescription": "브랜치 {branchName}가 완전히 병합되지 않았습니다. 강제 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "forceDeleteDescription": "브랜치 {branchName}가 완전히 병합되지 않았습니다. 강제 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "deleteRemoteTitle": "원격 브랜치 삭제", + "deleteRemoteDescription": "원격 브랜치 {branchName}을(를) 삭제하시겠습니까? 이 작업은 원격 저장소에서 브랜치를 제거하며 되돌릴 수 없습니다." }, "current": "현재", "switchToBranch": "이 브랜치로 전환", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 27413d8..503877d 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -914,6 +914,7 @@ "mergeBranch": "Mesclar {branchName}", "rebaseTo": "Rebase para {branchName}", "deleteBranch": "Excluir branch {branchName}", + "deleteRemoteBranch": "Excluir branch remoto {branchName}", "initGitRepo": "Inicializar repositório Git", "pullCode": "Fazer pull do código", "fetchInfo": "Buscar informações", @@ -929,7 +930,9 @@ "rebaseDescription": "Fazer rebase da branch atual {currentBranch} sobre {branchName}?", "deleteDescription": "Excluir a branch {branchName}? Esta ação não pode ser desfeita.", "forceDeleteTitle": "Forçar exclusão do branch", - "forceDeleteDescription": "O branch {branchName} não está totalmente mesclado. Tem certeza de que deseja forçar a exclusão? Esta ação não pode ser desfeita." + "forceDeleteDescription": "O branch {branchName} não está totalmente mesclado. Tem certeza de que deseja forçar a exclusão? Esta ação não pode ser desfeita.", + "deleteRemoteTitle": "Excluir branch remoto", + "deleteRemoteDescription": "Excluir o branch remoto {branchName}? Isso o removerá do repositório remoto e não poderá ser desfeito." }, "current": "Atual", "switchToBranch": "Mudar para esta branch", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index d62b451..4ef3e25 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -914,6 +914,7 @@ "mergeBranch": "合并 {branchName}", "rebaseTo": "变基到 {branchName}", "deleteBranch": "删除分支 {branchName}", + "deleteRemoteBranch": "删除远程分支 {branchName}", "initGitRepo": "初始化 Git 仓库", "pullCode": "更新代码", "fetchInfo": "获取信息", @@ -929,7 +930,9 @@ "rebaseDescription": "确定将当前分支 {currentBranch} 变基到 {branchName} 吗?", "deleteDescription": "确定删除分支 {branchName} 吗?此操作不可恢复。", "forceDeleteTitle": "强制删除分支", - "forceDeleteDescription": "分支 {branchName} 尚未完全合并,确定要强制删除吗?此操作不可恢复。" + "forceDeleteDescription": "分支 {branchName} 尚未完全合并,确定要强制删除吗?此操作不可恢复。", + "deleteRemoteTitle": "删除远程分支", + "deleteRemoteDescription": "确定删除远程分支 {branchName} 吗?此操作将从远程仓库中移除该分支,且不可恢复。" }, "current": "当前", "switchToBranch": "切换到此分支", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index d008b63..43bbb27 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -914,6 +914,7 @@ "mergeBranch": "合併 {branchName}", "rebaseTo": "變基到 {branchName}", "deleteBranch": "刪除分支 {branchName}", + "deleteRemoteBranch": "刪除遠端分支 {branchName}", "initGitRepo": "初始化 Git 倉庫", "pullCode": "更新程式碼", "fetchInfo": "獲取資訊", @@ -929,7 +930,9 @@ "rebaseDescription": "確定將目前分支 {currentBranch} 變基到 {branchName} 嗎?", "deleteDescription": "確定刪除分支 {branchName} 嗎?此操作無法復原。", "forceDeleteTitle": "強制刪除分支", - "forceDeleteDescription": "分支 {branchName} 尚未完全合併,確定要強制刪除嗎?此操作不可恢復。" + "forceDeleteDescription": "分支 {branchName} 尚未完全合併,確定要強制刪除嗎?此操作不可恢復。", + "deleteRemoteTitle": "刪除遠端分支", + "deleteRemoteDescription": "確定刪除遠端分支 {branchName} 嗎?此操作將從遠端倉庫中移除該分支,且不可恢復。" }, "current": "目前", "switchToBranch": "切換到此分支", diff --git a/src/lib/api.ts b/src/lib/api.ts index d196089..ed4038e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -644,6 +644,20 @@ export async function gitDeleteBranch( return getTransport().call("git_delete_branch", { path, branchName, force }) } +export async function gitDeleteRemoteBranch( + path: string, + remote: string, + branch: string, + credentials?: GitCredentials | null +): Promise { + return getTransport().call("git_delete_remote_branch", { + path, + remote, + branch, + credentials: credentials ?? null, + }) +} + export async function gitListConflicts(path: string): Promise { return getTransport().call("git_list_conflicts", { path }) }