feat(bisect): add guided Git bisect support
Add interactive Git bisect support to the Tauri backend and UI. Introduce BisectState and BisectCommit types for structured state. Expose Tauri commands to query, start, mark, and reset sessions. Add parsers, unit tests, and a Bisect dialog with toolbar and App wiring. - Backend: new Tauri commands, state types, and helpers to parse bisect. - Frontend: Bisect dialog, toolbar entry, and App integration to control flow.
This commit is contained in:
+81
-1
@@ -68,6 +68,7 @@
|
||||
compareFileToParent,
|
||||
fetchCommitNotes,
|
||||
fetchRemote,
|
||||
getBisectState,
|
||||
getCommitNote,
|
||||
getFileBlame,
|
||||
getGitLfsStatus,
|
||||
@@ -85,6 +86,7 @@
|
||||
listReflog,
|
||||
listRepositoryFiles,
|
||||
mergeBranch,
|
||||
markBisect,
|
||||
mergeAbort,
|
||||
mergeContinue,
|
||||
openRepoInExplorer,
|
||||
@@ -102,6 +104,7 @@
|
||||
removeRemote,
|
||||
removeWorktree,
|
||||
repairWorktree,
|
||||
resetBisect,
|
||||
revertCommit,
|
||||
setBranchUpstream,
|
||||
setCommitNote,
|
||||
@@ -128,6 +131,7 @@
|
||||
restoreFiles,
|
||||
restoreToCommit,
|
||||
searchCodeIntroductions,
|
||||
startBisect,
|
||||
startInteractiveRebase,
|
||||
setSyncBadge,
|
||||
stageFiles,
|
||||
@@ -151,6 +155,7 @@
|
||||
AppLanguage,
|
||||
AppTheme,
|
||||
AnalyticsSettings,
|
||||
BisectState,
|
||||
CloneOptions,
|
||||
CustomThemeColors,
|
||||
ConflictFile,
|
||||
@@ -423,6 +428,10 @@
|
||||
let reflogEntries: ReflogEntry[] = [];
|
||||
let reflogLoading = false;
|
||||
let reflogError = "";
|
||||
let bisectOpen = false;
|
||||
let bisectState: BisectState | null = null;
|
||||
let bisectLoading = false;
|
||||
let bisectError = "";
|
||||
let selectedDiffPath = "";
|
||||
let diffHighlightQuery = "";
|
||||
let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null;
|
||||
@@ -999,7 +1008,7 @@
|
||||
}
|
||||
|
||||
async function autoRefreshTick() {
|
||||
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || worktreeDialogOpen || gitLfsDialogOpen || newBranchCommit || commitNoteTarget || globalSearchOpen || helpOpen) return;
|
||||
if (appShuttingDown || !autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || bisectOpen || worktreeDialogOpen || gitLfsDialogOpen || newBranchCommit || commitNoteTarget || globalSearchOpen || helpOpen) return;
|
||||
const path = activeRepoPath;
|
||||
autoRefreshInFlight = true;
|
||||
try {
|
||||
@@ -3325,6 +3334,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function openBisect() {
|
||||
if (!hasRepository || isBusy) return;
|
||||
bisectOpen = true;
|
||||
bisectState = null;
|
||||
bisectError = "";
|
||||
bisectLoading = true;
|
||||
try {
|
||||
bisectState = await getBisectState(activeRepoPath);
|
||||
trackEvent("bisect_opened", { active: bisectState.active ? 1 : 0 });
|
||||
} catch (error) {
|
||||
bisectError = errorToMessage(error);
|
||||
} finally {
|
||||
bisectLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function beginBisect(good: string, bad: string) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
bisectError = "";
|
||||
await runOperation("Starting Git bisect", async () => {
|
||||
bisectState = await startBisect(activeRepoPath, good, bad);
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
trackEvent("bisect_started");
|
||||
});
|
||||
if (bisectOpen && errorMessage) bisectError = errorMessage;
|
||||
}
|
||||
|
||||
async function continueBisect(verdict: "good" | "bad" | "skip") {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
bisectError = "";
|
||||
await runOperation("Continuing Git bisect", async () => {
|
||||
bisectState = await markBisect(activeRepoPath, verdict);
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
trackEvent("bisect_commit_marked", { verdict, finished: bisectState.finished ? 1 : 0 });
|
||||
});
|
||||
if (bisectOpen && errorMessage) bisectError = errorMessage;
|
||||
}
|
||||
|
||||
async function stopBisect() {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
bisectError = "";
|
||||
await runOperation("Stopping Git bisect", async () => {
|
||||
applyStatus(await resetBisect(activeRepoPath));
|
||||
bisectOpen = false;
|
||||
bisectState = null;
|
||||
await refreshRepositoryViews(activeRepoPath);
|
||||
trackEvent("bisect_stopped");
|
||||
});
|
||||
if (bisectOpen && errorMessage) bisectError = errorMessage;
|
||||
}
|
||||
|
||||
async function previewReflogEntry(entry: ReflogEntry) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
await runOperation("Previewing reflog entry", async () => {
|
||||
@@ -5041,6 +5101,7 @@
|
||||
else if (event.key === "Escape" && gitLfsDialogOpen) closeGitLfsDialog();
|
||||
else if (event.key === "Escape" && interactiveRebaseOpen && !isBusy) interactiveRebaseOpen = false;
|
||||
else if (event.key === "Escape" && reflogOpen && !isBusy) reflogOpen = false;
|
||||
else if (event.key === "Escape" && bisectOpen && !isBusy) bisectOpen = false;
|
||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
||||
}
|
||||
@@ -5106,6 +5167,7 @@
|
||||
onCompare={openCompareSelect}
|
||||
onInteractiveRebase={openInteractiveRebase}
|
||||
onReflog={openReflog}
|
||||
onBisect={openBisect}
|
||||
onOpenInEditor={openActiveRepoInEditor}
|
||||
onOpenTerminal={openActiveRepoTerminal}
|
||||
onOpenInExplorer={openActiveRepoFileManager}
|
||||
@@ -5579,6 +5641,7 @@
|
||||
onOpenSearch={openGlobalSearchDialog}
|
||||
onOpenCompare={openCompareSelect}
|
||||
onOpenReflog={openReflog}
|
||||
onOpenBisect={openBisect}
|
||||
onOpenInteractiveRebase={openInteractiveRebase}
|
||||
onOpenWorktrees={openWorktreeDialog}
|
||||
onOpenSyncSettings={openSyncOptions}
|
||||
@@ -5879,6 +5942,23 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if bisectOpen}
|
||||
{#await import("./lib/components/BisectDialog.svelte") then module}
|
||||
<module.default
|
||||
language={appLanguage}
|
||||
{bisectState}
|
||||
isLoading={bisectLoading}
|
||||
{isBusy}
|
||||
{operation}
|
||||
error={bisectError}
|
||||
onStart={beginBisect}
|
||||
onMark={continueBisect}
|
||||
onReset={stopBisect}
|
||||
onClose={() => { if (!isBusy) bisectOpen = false; }}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<!-- Compare: pick two branches or commits to diff -->
|
||||
{#if compareSelectOpen}
|
||||
<CompareSelectDialog
|
||||
|
||||
Reference in New Issue
Block a user