feat(branches): add force delete flow for local branches
publish / publish-tauri (, windows-latest) (release) Successful in 22m57s
publish / publish-tauri (, windows-latest) (release) Successful in 22m57s
Branch deletion now supports an optional force mode in the Tauri command, enabling deletion of branches that are not fully merged. The UI replaces the simple confirm prompt with a dedicated dialog and auto-escalates to force delete when Git rejects a normal delete. - Add force flag to delete_branch command and tests - Implement BranchDeleteConfirmDialog and wire it into App.svelte - Update branch context menu actions and styling
This commit is contained in:
+52
-5
@@ -6,6 +6,7 @@
|
||||
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
||||
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
||||
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
||||
@@ -172,6 +173,8 @@
|
||||
let comparison: GitCommitComparison | null = null;
|
||||
let newBranchCommit: GitCommit | null = null;
|
||||
let renameBranchTarget: GitBranchInfo | null = null;
|
||||
let deleteBranchTarget: GitBranchInfo | null = null;
|
||||
let deleteBranchForce = false;
|
||||
let compareSelectOpen = false;
|
||||
let compareDialogOpen = false;
|
||||
let selectedDiffPath = "";
|
||||
@@ -777,6 +780,8 @@
|
||||
pendingRestoreFile = null;
|
||||
newBranchCommit = null;
|
||||
globalSearchResults = [];
|
||||
deleteBranchTarget = null;
|
||||
deleteBranchForce = false;
|
||||
globalSearchOpen = false;
|
||||
globalSearchError = "";
|
||||
resolveDialogOpen = false;
|
||||
@@ -812,6 +817,11 @@
|
||||
return (value?.files ?? []).some((file) => file.staged === "conflicted" || file.unstaged === "conflicted");
|
||||
}
|
||||
|
||||
function isBranchNotFullyMergedError(message: string): boolean {
|
||||
const value = message.toLowerCase();
|
||||
return value.includes("not fully merged") || value.includes("run 'git branch -d'");
|
||||
}
|
||||
|
||||
async function runOperation(label: string, task: () => Promise<void>) {
|
||||
if (isBusy) return;
|
||||
operation = label;
|
||||
@@ -1084,16 +1094,41 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(`Delete local branch "${branch.name}"?\n\nGit will refuse if the branch has unmerged changes.`);
|
||||
if (!confirmed) return;
|
||||
deleteBranchTarget = branch;
|
||||
deleteBranchForce = false;
|
||||
}
|
||||
|
||||
await runOperation(`Deleting ${branch.name}`, async () => {
|
||||
applyStatus(await deleteBranch(activeRepoPath, branch.name));
|
||||
async function confirmDeleteBranch() {
|
||||
const branch = deleteBranchTarget;
|
||||
if (!activeRepoPath || !branch || branch.remote || branch.current || isBusy) return;
|
||||
|
||||
operation = `${deleteBranchForce ? "Force deleting" : "Deleting"} ${branch.name}`;
|
||||
errorMessage = "";
|
||||
try {
|
||||
applyStatus(await deleteBranch(activeRepoPath, branch.name, deleteBranchForce));
|
||||
deleteBranchTarget = null;
|
||||
deleteBranchForce = false;
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
});
|
||||
} catch (error) {
|
||||
const message = errorToMessage(error);
|
||||
if (deleteBranchForce || !isBranchNotFullyMergedError(message)) {
|
||||
errorMessage = message;
|
||||
return;
|
||||
}
|
||||
|
||||
deleteBranchForce = true;
|
||||
} finally {
|
||||
operation = "";
|
||||
}
|
||||
}
|
||||
|
||||
function closeDeleteBranchDialog() {
|
||||
if (isBusy) return;
|
||||
deleteBranchTarget = null;
|
||||
deleteBranchForce = false;
|
||||
}
|
||||
|
||||
function openNewBranchDialog(commit: GitCommit) {
|
||||
@@ -1881,6 +1916,7 @@
|
||||
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
||||
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
|
||||
else if (event.key === "Escape" && deleteBranchTarget) closeDeleteBranchDialog();
|
||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
||||
}
|
||||
@@ -2374,6 +2410,17 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Delete a local branch from the branch context menu -->
|
||||
{#if deleteBranchTarget}
|
||||
<BranchDeleteConfirmDialog
|
||||
branch={deleteBranchTarget}
|
||||
force={deleteBranchForce}
|
||||
{isBusy}
|
||||
onConfirm={confirmDeleteBranch}
|
||||
onClose={closeDeleteBranchDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Choose the AI provider/model used to generate commit messages -->
|
||||
{#if aiSettingsOpen}
|
||||
<AiSettingsDialog
|
||||
|
||||
Reference in New Issue
Block a user