refactor(status): Support batch and all changes discard functionality

The file discarding mechanism has been significantly refactored to improve handling of multi-file operations. Instead of processing discards on a per-file basis, the system now supports batch actions for selected files or reverting all tracked modifications simultaneously. This update centralizes complex discard logic into dedicated functions across the component suite.

- Updated state management and types to handle arrays of file changes.
- Added UI elements and handlers for discarding all staged/unstaged changes.
- Enhanced the confirmation dialog to display multiple discarded targets in a list view.
This commit is contained in:
Christoph Brandau
2026-07-10 11:36:15 +02:00
parent 44a5b776d7
commit 7ef1a2dac8
4 changed files with 134 additions and 26 deletions
+48 -11
View File
@@ -133,7 +133,8 @@
type AppView = "management" | "repository";
type CredentialAction = "push" | "pull" | "fetch" | "clone";
type PendingDiscard =
| { kind: "file"; file: GitFileStatus; staged: boolean }
| { kind: "file"; files: GitFileStatus[]; staged: boolean }
| { kind: "all-changes"; files: GitFileStatus[] }
| { kind: "hunk"; file: GitFileStatus; staged: boolean; action: PatchApplyAction; patch: string };
interface RepoTab {
@@ -2588,26 +2589,59 @@
});
}
function discardFile(file: GitFileStatus, staged: boolean) {
if (!activeRepoPath || isBusy) return;
pendingDiscard = { kind: "file", file, staged };
function discardFiles(files: GitFileStatus[], staged: boolean) {
if (!activeRepoPath || isBusy || files.length === 0) return;
pendingDiscard = { kind: "file", files, staged };
trackEvent("discard_confirm_opened", {
kind: "file",
staged: staged ? 1 : 0,
files: files.length,
});
}
async function runDiscardFile(file: GitFileStatus, staged: boolean) {
await runOperation(`Discarding ${file.path}`, async () => {
applyStatus(await restoreFiles(activeRepoPath, [file.path], staged));
function discardChanges(files: GitFileStatus[]) {
if (!activeRepoPath || isBusy || files.length === 0) return;
pendingDiscard = { kind: "all-changes", files };
trackEvent("discard_confirm_opened", {
kind: "all",
files: files.length,
});
}
async function runDiscardFiles(files: GitFileStatus[], staged: boolean) {
if (files.length === 0) return;
const paths = files.map((file) => file.path);
await runOperation(files.length === 1 ? `Discarding ${baseName(files[0].path)}` : `Discarding ${files.length} files`, async () => {
applyStatus(await restoreFiles(activeRepoPath, paths, staged));
await refreshExplorerFiles(activeRepoPath);
await refreshFileHistory(activeRepoPath);
trackEvent("file_discarded", {
files: files.length,
staged: staged ? 1 : 0,
});
});
}
// Discards both the staged and unstaged changes for each given file (used
// by "Discard all" and "Discard selected", which don't distinguish lanes).
async function runDiscardAllChanges(files: GitFileStatus[]) {
const stagedPaths = files.filter((file) => file.staged !== null).map((file) => file.path);
const unstagedPaths = files.filter((file) => file.unstaged !== null).map((file) => file.path);
if (stagedPaths.length === 0 && unstagedPaths.length === 0) return;
await runOperation(files.length === 1 ? `Discarding ${baseName(files[0].path)}` : `Discarding ${files.length} files`, async () => {
let nextStatus: GitStatus | null = null;
if (stagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, stagedPaths, true);
if (unstagedPaths.length > 0) nextStatus = await restoreFiles(activeRepoPath, unstagedPaths, false);
if (nextStatus) applyStatus(nextStatus);
await refreshExplorerFiles(activeRepoPath);
await refreshFileHistory(activeRepoPath);
trackEvent("file_discarded", {
files: files.length,
staged: 2,
});
});
}
async function openLinePatch(file: GitFileStatus, staged: boolean) {
if (!activeRepoPath) return;
linePatchOpen = true;
@@ -2743,7 +2777,9 @@
if (!discard || !activeRepoPath || isBusy) return;
if (discard.kind === "file") {
await runDiscardFile(discard.file, discard.staged);
await runDiscardFiles(discard.files, discard.staged);
} else if (discard.kind === "all-changes") {
await runDiscardAllChanges(discard.files);
} else {
await runLinePatchAction(discard.action, discard.patch, discard.file, discard.staged);
}
@@ -3750,7 +3786,8 @@
onSelectFile={selectFileFromStatus}
onStage={stageFile}
onUnstage={unstageFile}
onDiscard={discardFile}
onDiscard={discardFiles}
onDiscardMany={discardChanges}
onPatch={openLinePatch}
onStageAll={stageAllFiles}
onUnstageAll={unstageAllFiles}
@@ -3927,8 +3964,8 @@
{#if pendingDiscard}
<DiscardConfirmDialog
file={pendingDiscard.file}
staged={pendingDiscard.staged}
files={pendingDiscard.kind === "hunk" ? [pendingDiscard.file] : pendingDiscard.files}
staged={pendingDiscard.kind === "all-changes" ? null : pendingDiscard.staged}
scope={pendingDiscard.kind === "hunk" ? "hunk" : "file"}
{isBusy}
onConfirm={confirmDiscard}