feat: add selective line restoration from historical commits
Add a new Tauri command to produce a diff between the working file and a historical commit (get_file_restore_patch) and support a new apply action ("restore-lines") that validates and applies only text-line changes for a single regular file.
Behavior changes and constraints:
- Fetch a filtered reverse diff for a file in a commit so UI can display selectable lines from an older revision.
- Applying "restore-lines" verifies the target is a regular file, rejects binary/metadata patches, and ensures the patch only modifies the selected file.
- Restored lines are applied to the working tree without staging other changes; the index is preserved.
- The operation rejects stale patches or patches targeting the wrong file.
UI wiring:
- Compare dialog gets a "Restore lines…" action for applicable modified files and opens the line-patch dialog in restore mode.
- Line-patch dialog gains a restore mode (restoreCommit) with adjusted UI/rendering to pair removed/added lines, helper text, and dedicated "Restore selected" / "Restore hunk" actions.
- App integration handles fetching the restore patch, applying selected lines, and refreshing views.
Tests:
- Add tests covering correct behavior (preserve unstaged/staged changes and index) and guard cases (stale/wrong-file patches).
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
cancelCodeSearch,
|
||||
cancelFileHistory,
|
||||
applyFilePatch,
|
||||
getFileRestorePatch,
|
||||
createBranch,
|
||||
createTag,
|
||||
deleteBranch,
|
||||
@@ -495,6 +496,8 @@
|
||||
let selectedDiffPath = "";
|
||||
let diffHighlightQuery = "";
|
||||
let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null;
|
||||
let linePatchRestoreCommit = "";
|
||||
let linePatchRestoreRepo = "";
|
||||
let linePatchOpen = false;
|
||||
let linePatchFile: GitFileStatus | null = null;
|
||||
let linePatchStaged = false;
|
||||
@@ -5019,6 +5022,8 @@
|
||||
|
||||
async function openLinePatch(file: GitFileStatus, staged: boolean) {
|
||||
if (!activeRepoPath) return;
|
||||
linePatchRestoreCommit = "";
|
||||
linePatchRestoreRepo = "";
|
||||
linePatchOpen = true;
|
||||
linePatchFile = file;
|
||||
linePatchStaged = staged;
|
||||
@@ -5039,13 +5044,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function openHistoricalLineRestore() {
|
||||
if (!activeRepoPath || !comparison || comparison.to_hash || isBusy) return;
|
||||
const file = comparison.files.find(item => item.path === selectedDiffPath);
|
||||
if (!file || file.status !== "modified" || file.old_path) return;
|
||||
linePatchRestoreCommit = comparison.from_hash;
|
||||
linePatchRestoreRepo = activeRepoPath;
|
||||
linePatchFile = { path: file.path, old_path: null, staged: null, unstaged: "modified" };
|
||||
linePatchStaged = false;
|
||||
linePatchText = "";
|
||||
linePatchError = "";
|
||||
compareDialogOpen = false;
|
||||
fileHistoryDialogOpen = false;
|
||||
globalSearchOpen = false;
|
||||
linePatchOpen = true;
|
||||
await refreshLinePatch();
|
||||
}
|
||||
|
||||
async function restoreSelectedLines(patch: string) {
|
||||
if (!linePatchFile || !linePatchRestoreCommit || isBusy) return;
|
||||
const file = linePatchFile.path;
|
||||
const repo = linePatchRestoreRepo;
|
||||
const commit = linePatchRestoreCommit;
|
||||
if (repo !== activeRepoPath) { linePatchError = "The active repository changed. Reopen the comparison."; return; }
|
||||
operation = appLanguage === "de" ? "Ausgewählte Zeilen wiederherstellen" : "Restoring selected lines";
|
||||
linePatchError = "";
|
||||
try {
|
||||
applyStatus(await applyFilePatch(repo, file, patch, "restore-lines"));
|
||||
linePatchText = await getFileRestorePatch(repo, commit, file);
|
||||
comparison = await diffFileAgainstWorkingTree(repo, commit, file);
|
||||
await refreshRepositoryViews(repo, { branches: false, commits: false });
|
||||
await refreshFileHistory(repo, file, true);
|
||||
} catch (error) { linePatchError = errorToMessage(error); }
|
||||
finally { operation = ""; }
|
||||
}
|
||||
|
||||
async function refreshLinePatch() {
|
||||
if (!activeRepoPath || !linePatchFile) return;
|
||||
if (linePatchRestoreCommit) {
|
||||
linePatchLoading = true;
|
||||
linePatchError = "";
|
||||
try { linePatchText = await getFileRestorePatch(linePatchRestoreRepo, linePatchRestoreCommit, linePatchFile.path); }
|
||||
catch (error) { linePatchError = errorToMessage(error); }
|
||||
finally { linePatchLoading = false; }
|
||||
return;
|
||||
}
|
||||
await openLinePatch(linePatchFile, linePatchStaged);
|
||||
}
|
||||
|
||||
function closeLinePatch() {
|
||||
if (isBusy) return;
|
||||
if (linePatchRestoreCommit) compareDialogOpen = !!comparison;
|
||||
linePatchRestoreCommit = "";
|
||||
linePatchRestoreRepo = "";
|
||||
linePatchOpen = false;
|
||||
linePatchFile = null;
|
||||
linePatchText = "";
|
||||
@@ -5132,6 +5183,7 @@
|
||||
}
|
||||
|
||||
async function applyLinePatch(action: PatchApplyAction, patch: string, scope: "hunk" | "lines") {
|
||||
if (action === "restore-lines") { await restoreSelectedLines(patch); return; }
|
||||
if (!activeRepoPath || !linePatchFile || isBusy) return;
|
||||
const file = linePatchFile;
|
||||
const staged = linePatchStaged;
|
||||
@@ -6681,6 +6733,7 @@
|
||||
<module.default
|
||||
file={linePatchFile}
|
||||
staged={linePatchStaged}
|
||||
restoreCommit={linePatchRestoreCommit}
|
||||
patch={linePatchText}
|
||||
{isBusy}
|
||||
isLoading={linePatchLoading}
|
||||
@@ -6947,6 +7000,7 @@
|
||||
restoreLabel={pendingRestoreFile ? (appLanguage === "de" ? "Datei wiederherstellen" : "Restore file") : ""}
|
||||
onClose={closeCompareDialog}
|
||||
onRestore={restorePreviewedCommitFile}
|
||||
onRestoreLines={openHistoricalLineRestore}
|
||||
onSelectFile={selectDiffFile}
|
||||
/>
|
||||
{/await}
|
||||
|
||||
Reference in New Issue
Block a user