Merge branch 'opt/light_mode'
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m59s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 27m53s

This commit is contained in:
Christoph Brandau
2026-07-10 22:59:20 +02:00
9 changed files with 1389 additions and 106 deletions
+154 -1
View File
@@ -22,9 +22,11 @@
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte";
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
import RenameBranchDialog from "./lib/components/RenameBranchDialog.svelte";
import ReflogDialog from "./lib/components/ReflogDialog.svelte";
import RepoLoadingOverlay from "./lib/components/RepoLoadingOverlay.svelte";
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
import StashPanel from "./lib/components/StashPanel.svelte";
@@ -62,6 +64,8 @@
listTags,
listCommits,
listFileHistory,
listInteractiveRebaseCommits,
listReflog,
listRepositoryFiles,
mergeBranch,
openRepoInExplorer,
@@ -83,9 +87,11 @@
resolveConflict,
resolveConflictSide,
restoreFileFromCommit,
restoreReflogEntry,
restoreFiles,
restoreToCommit,
searchCodeIntroductions,
startInteractiveRebase,
setSyncBadge,
stageFiles,
stashApply,
@@ -119,6 +125,9 @@
LocalModelOption,
PatchApplyAction,
PreparedResolution,
RebaseCommit,
RebasePlanItem,
ReflogEntry,
StoredCredential,
} from "./lib/types";
@@ -254,6 +263,15 @@
let deleteBranchForce = false;
let compareSelectOpen = false;
let compareDialogOpen = false;
let interactiveRebaseOpen = false;
let interactiveRebaseBase = "";
let interactiveRebaseCommits: RebaseCommit[] = [];
let interactiveRebaseLoading = false;
let interactiveRebaseError = "";
let reflogOpen = false;
let reflogEntries: ReflogEntry[] = [];
let reflogLoading = false;
let reflogError = "";
let selectedDiffPath = "";
let diffHighlightQuery = "";
let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null;
@@ -654,7 +672,7 @@
}
async function autoRefreshTick() {
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || newBranchCommit || globalSearchOpen) return;
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen) return;
const path = activeRepoPath;
autoRefreshInFlight = true;
try {
@@ -1580,6 +1598,13 @@
comparison = null;
compareSelectOpen = false;
compareDialogOpen = false;
interactiveRebaseOpen = false;
interactiveRebaseBase = "";
interactiveRebaseCommits = [];
interactiveRebaseError = "";
reflogOpen = false;
reflogEntries = [];
reflogError = "";
selectedDiffPath = "";
pendingRestoreFile = null;
newBranchCommit = null;
@@ -2250,6 +2275,99 @@
});
}
function preferredInteractiveRebaseBase(): string {
const candidates = [status?.upstream, "origin/main", "main", "origin/master", "master"]
.filter((value): value is string => Boolean(value) && value !== status?.current_branch);
for (const candidate of candidates) {
if (branches.some((branch) => branch.name === candidate)) return candidate;
}
return branches.find((branch) => !branch.current)?.name ?? "";
}
async function loadInteractiveRebaseRange(base: string) {
interactiveRebaseBase = base;
interactiveRebaseCommits = [];
interactiveRebaseError = "";
if (!activeRepoPath || !base) return;
interactiveRebaseLoading = true;
try {
const result = await listInteractiveRebaseCommits(activeRepoPath, base);
if (interactiveRebaseBase === base) interactiveRebaseCommits = result;
} catch (error) {
if (interactiveRebaseBase === base) interactiveRebaseError = errorToMessage(error);
} finally {
if (interactiveRebaseBase === base) interactiveRebaseLoading = false;
}
}
function openInteractiveRebase() {
if (!hasRepository || rebaseInProgress || cherryPickInProgress || isBusy) return;
interactiveRebaseOpen = true;
const base = preferredInteractiveRebaseBase();
void loadInteractiveRebaseRange(base);
trackEvent("interactive_rebase_opened");
}
async function runInteractiveRebase(plan: RebasePlanItem[]) {
if (!activeRepoPath || !interactiveRebaseBase || isBusy) return;
interactiveRebaseError = "";
await runOperation("Starting interactive rebase", async () => {
applyStatus(await startInteractiveRebase(activeRepoPath, interactiveRebaseBase, plan));
interactiveRebaseOpen = false;
await refreshBranchList(activeRepoPath);
await refreshCommitHistory(activeRepoPath);
await refreshExplorerFiles(activeRepoPath);
await refreshFileHistory(activeRepoPath);
trackEvent("interactive_rebase_started", { commits: plan.length });
});
if (interactiveRebaseOpen && errorMessage) interactiveRebaseError = errorMessage;
}
async function openReflog() {
if (!hasRepository || isBusy) return;
reflogOpen = true;
reflogEntries = [];
reflogError = "";
reflogLoading = true;
try {
reflogEntries = await listReflog(activeRepoPath, 300);
trackEvent("reflog_opened", { entries: reflogEntries.length });
} catch (error) {
reflogError = errorToMessage(error);
} finally {
reflogLoading = false;
}
}
async function previewReflogEntry(entry: ReflogEntry) {
if (!activeRepoPath || isBusy) return;
await runOperation("Previewing reflog entry", async () => {
const result = await compareCommits(activeRepoPath, entry.hash, "HEAD");
comparison = result;
selectedDiffPath = result.files[0]?.path ?? "";
diffHighlightQuery = "";
pendingRestoreFile = null;
reflogOpen = false;
compareDialogOpen = true;
trackEvent("reflog_previewed", { files: result.files.length });
});
}
async function recoverReflogEntry(entry: ReflogEntry, branch: string) {
if (!activeRepoPath || !branch.trim() || isBusy) return;
reflogError = "";
await runOperation("Restoring reflog entry", async () => {
applyStatus(await restoreReflogEntry(activeRepoPath, entry.hash, branch.trim()));
reflogOpen = false;
await refreshBranchList(activeRepoPath);
await refreshCommitHistory(activeRepoPath);
await refreshExplorerFiles(activeRepoPath);
await refreshFileHistory(activeRepoPath);
trackEvent("reflog_recovered");
});
if (reflogOpen && errorMessage) reflogError = errorMessage;
}
async function createNewTag(name: string, message: string) {
const trimmed = name.trim();
if (!activeRepoPath || !trimmed) return;
@@ -3337,6 +3455,8 @@
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" && interactiveRebaseOpen && !isBusy) interactiveRebaseOpen = false;
else if (event.key === "Escape" && reflogOpen && !isBusy) reflogOpen = false;
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
}
@@ -3374,6 +3494,8 @@
onRefresh={refreshRepo}
onSearch={openGlobalSearchDialog}
onCompare={openCompareSelect}
onInteractiveRebase={openInteractiveRebase}
onReflog={openReflog}
onOpenInExplorer={openActiveRepoInExplorer}
onToggleAutoRefresh={toggleAutoRefresh}
onOpenSettings={() => { appSettingsOpen = true; }}
@@ -4093,6 +4215,37 @@
/>
{/if}
<!-- Compare: pick the two commits to diff -->
{#if interactiveRebaseOpen}
<InteractiveRebaseDialog
{branches}
currentBranch={status?.current_branch ?? ""}
base={interactiveRebaseBase}
commits={interactiveRebaseCommits}
isLoading={interactiveRebaseLoading}
{isBusy}
{operation}
error={interactiveRebaseError}
onBaseChange={loadInteractiveRebaseRange}
onStart={runInteractiveRebase}
onClose={() => { if (!isBusy) interactiveRebaseOpen = false; }}
/>
{/if}
{#if reflogOpen}
<ReflogDialog
entries={reflogEntries}
currentHash={reflogEntries.find((entry) => entry.selector === "HEAD@{0}")?.hash ?? ""}
isLoading={reflogLoading}
{isBusy}
{operation}
error={reflogError}
onPreview={previewReflogEntry}
onRestore={recoverReflogEntry}
onClose={() => { if (!isBusy) reflogOpen = false; }}
/>
{/if}
<!-- Compare: pick the two commits to diff -->
{#if compareSelectOpen}
<CompareSelectDialog
+314 -78
View File
@@ -44,6 +44,27 @@
--app-settings-row-bg: #141b29;
--app-scrollbar-thumb: #303a4f;
--app-scrollbar-thumb-hover: #44506a;
--code-surface: #111321;
--code-surface-raised: #171a2b;
--code-surface-subtle: #0d101a;
--code-surface-muted: #10131e;
--code-surface-meta: #0c0e18;
--code-input-bg: #0b0e18;
--code-hover-bg: #151a2b;
--code-add-text: #5dd88a;
--code-add-strong: #4eca76;
--code-add-bg: rgba(78, 202, 118, 0.09);
--code-add-gutter-bg: rgba(78, 202, 118, 0.1);
--code-delete-text: #ef8080;
--code-delete-strong: #e86060;
--code-delete-bg: rgba(232, 96, 96, 0.1);
--code-delete-gutter-bg: rgba(232, 96, 96, 0.12);
--code-hunk-text: #7aacff;
--code-hunk-bg: rgba(122, 172, 255, 0.08);
--code-match-text: #f3c969;
--code-match-bg: rgba(240, 182, 72, 0.22);
--code-match-gutter-bg: rgba(240, 182, 72, 0.2);
}
:root[data-theme="light"] {
@@ -89,6 +110,27 @@
--app-settings-row-bg: #f8fafd;
--app-scrollbar-thumb: #c2cada;
--app-scrollbar-thumb-hover: #aeb8cb;
--code-surface: #fbfcfe;
--code-surface-raised: #f1f5fa;
--code-surface-subtle: #f3f6fb;
--code-surface-muted: #edf1f7;
--code-surface-meta: #e9eef7;
--code-input-bg: #ffffff;
--code-hover-bg: #e8eef7;
--code-add-text: #146c37;
--code-add-strong: #19723d;
--code-add-bg: rgba(25, 114, 61, 0.1);
--code-add-gutter-bg: rgba(25, 114, 61, 0.14);
--code-delete-text: #a91f36;
--code-delete-strong: #b4233b;
--code-delete-bg: rgba(180, 35, 59, 0.09);
--code-delete-gutter-bg: rgba(180, 35, 59, 0.13);
--code-hunk-text: #245cc7;
--code-hunk-bg: rgba(36, 92, 199, 0.09);
--code-match-text: #744500;
--code-match-bg: rgba(230, 158, 31, 0.2);
--code-match-gutter-bg: rgba(230, 158, 31, 0.24);
}
@layer base {
@@ -568,7 +610,8 @@
}
.titlebar-info {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 6px;
padding: 0 12px;
@@ -576,12 +619,20 @@
height: 100%;
overflow: hidden;
}
.titlebar-info svg { color: var(--color-accent); flex-shrink: 0; }
.titlebar-context {
display: flex;
align-items: center;
min-width: 0;
gap: 6px;
overflow: hidden;
}
.titlebar-context svg { color: var(--color-accent); flex: 0 0 auto; }
.tb-repo { color: var(--color-bar-muted); font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 160px; }
.tb-sep { color: rgba(255,255,255,0.22); font-size: 13px; }
.tb-branch { color: #f5f7ff; font-size: 12px; font-weight: 700; font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
.tb-repo { flex: 0 1 160px; min-width: 0; color: var(--color-bar-muted); font-size: 12px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-sep { flex: 0 0 auto; color: rgba(255,255,255,0.22); font-size: 13px; }
.tb-branch { flex: 1 1 auto; min-width: 0; max-width: 220px; color: #f5f7ff; font-size: 12px; font-weight: 700; font-family: var(--font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-sync-group { display: inline-flex; align-items: center; gap: 4px; min-width: max-content; }
.tb-sync { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 999px; font-size: 11px; font-weight: 800; white-space: nowrap; flex-shrink: 0; }
.tb-sync.ahead { color: #e0a040; background: rgba(224,160,64,0.13); }
.tb-sync.behind { color: #7aacff; background: rgba(122,172,255,0.13); }
@@ -2735,6 +2786,133 @@
max-height: calc(100vh - 32px);
overflow: auto;
}
.interactive-rebase-dialog,
.reflog-dialog {
grid-template-rows: auto minmax(0, 1fr) auto;
width: min(1120px, calc(100vw - 32px));
height: min(820px, 100%);
}
.interactive-rebase-body {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
align-content: start;
min-height: 0;
overflow: hidden;
}
.rebase-base-bar {
display: grid;
grid-template-columns: minmax(280px, 0.7fr) minmax(260px, 1fr);
align-items: end;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--color-surface-dim);
}
.rebase-base-bar label { display: grid; gap: 5px; color: var(--color-ink-muted); font-size: 12px; }
.rebase-base-bar label > span { font-weight: 700; }
.rebase-base-bar label strong { color: var(--color-ink); font-family: var(--font-mono); }
.rebase-base-bar p { margin: 0 0 4px; color: var(--color-ink-faint); font-size: 12px; line-height: 1.45; }
.rebase-plan {
display: grid;
align-content: start;
min-height: 0;
padding: 8px;
overflow: auto;
background: var(--code-surface);
}
.rebase-plan-row {
display: grid;
grid-template-columns: auto 112px 64px minmax(0, 1fr);
align-items: center;
gap: 8px;
min-height: 48px;
padding: 6px 8px;
border: 1px solid transparent;
border-bottom-color: var(--color-border-subtle);
background: var(--code-surface);
}
.rebase-plan-row:hover { border-color: var(--color-border-subtle); background: var(--code-hover-bg); }
.rebase-plan-row.drop { opacity: 0.58; background: var(--code-delete-bg); }
.rebase-order-actions { display: inline-flex; gap: 3px; }
.rebase-order-actions button {
width: 25px;
min-height: 25px;
padding: 0;
border-radius: 5px;
background: var(--code-surface-subtle);
}
.rebase-action { height: 30px; font-family: var(--font-mono); font-weight: 800; }
.rebase-action.pick { color: var(--code-add-strong); }
.rebase-action.reword { color: var(--code-hunk-text); }
.rebase-action.squash, .rebase-action.fixup { color: #96620f; }
.rebase-action.drop { color: var(--code-delete-strong); }
.rebase-plan-row > code { color: var(--color-accent); font-family: var(--font-mono); font-size: 11px; font-weight: 800; }
.rebase-commit-copy { display: grid; gap: 3px; min-width: 0; }
.rebase-commit-copy strong { overflow: hidden; color: var(--color-ink); font-size: 12.5px; text-overflow: ellipsis; white-space: nowrap; }
.rebase-commit-copy span { color: var(--color-ink-faint); font-size: 10.5px; }
.rebase-commit-copy input { height: 30px; font-family: var(--font-mono); font-size: 12px; }
.rebase-warning {
display: flex;
align-items: center;
gap: 7px;
margin: 8px 12px 0;
padding: 8px 10px;
border: 1px solid rgba(224,160,64,0.25);
border-radius: 7px;
color: #b87914;
background: rgba(224,160,64,0.08);
font-size: 12px;
}
.rebase-warning.error { border-color: rgba(232,96,96,0.28); color: var(--code-delete-text); background: var(--code-delete-bg); }
.rebase-footer-actions { display: flex; gap: 8px; }
.reflog-body { display: grid; grid-template-columns: minmax(340px, 0.8fr) minmax(0, 1.2fr); min-height: 0; overflow: hidden; }
.reflog-list-pane { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; min-height: 0; border-right: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.reflog-search { position: relative; display: flex; align-items: center; padding: 10px; border-bottom: 1px solid var(--color-border-subtle); }
.reflog-search svg { position: absolute; left: 21px; color: var(--color-ink-faint); }
.reflog-search input { padding-left: 34px; }
.reflog-list { display: grid; align-content: start; min-height: 0; padding: 7px; overflow: auto; }
.reflog-list > button {
display: grid;
justify-content: stretch;
gap: 4px;
width: 100%;
min-height: 70px;
padding: 8px 10px;
border-color: transparent;
border-bottom-color: var(--color-border-subtle);
border-radius: 6px;
background: transparent;
text-align: left;
}
.reflog-list > button:hover:not(:disabled) { background: var(--color-surface-hover); }
.reflog-list > button.active { border-color: rgba(49,95,214,0.3); background: rgba(49,95,214,0.09); }
.reflog-list > button strong { overflow: hidden; color: var(--color-ink); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.reflog-row-top, .reflog-row-bottom { display: flex; align-items: center; justify-content: space-between; min-width: 0; gap: 8px; color: var(--color-ink-faint); font-size: 10.5px; }
.reflog-row-top code { color: var(--color-accent); font-weight: 800; }
.reflog-row-bottom code { color: var(--color-ink-dim); }
.reflog-detail { display: grid; align-content: start; gap: 14px; min-width: 0; padding: 18px; overflow: auto; }
.reflog-detail-head { display: flex; align-items: center; gap: 10px; }
.reflog-detail-head > svg { color: var(--color-accent); }
.reflog-detail-head h3 { margin: 2px 0 0; color: var(--color-ink); font-size: 17px; }
.reflog-detail dl { display: grid; gap: 1px; margin: 0; overflow: hidden; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-border-subtle); }
.reflog-detail dl > div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; padding: 9px 11px; background: var(--color-surface-raised); }
.reflog-detail dt { color: var(--color-ink-faint); font-size: 11px; font-weight: 800; text-transform: uppercase; }
.reflog-detail dd { min-width: 0; margin: 0; overflow: hidden; color: var(--color-ink-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.reflog-detail dd code { font-family: var(--font-mono); }
.reflog-preview { justify-self: start; }
.reflog-recovery-card { display: grid; gap: 12px; padding: 14px; border: 1px solid rgba(78,202,118,0.24); border-radius: 10px; background: rgba(78,202,118,0.07); }
.reflog-recovery-title { display: flex; align-items: flex-start; gap: 9px; }
.reflog-recovery-title > svg { flex: 0 0 auto; color: var(--code-add-strong); }
.reflog-recovery-title div { display: grid; gap: 3px; }
.reflog-recovery-title strong { color: var(--color-ink); font-size: 13px; }
.reflog-recovery-title span { color: var(--color-ink-faint); font-size: 11px; line-height: 1.4; }
.reflog-recovery-card label { display: grid; gap: 5px; color: var(--color-ink-faint); font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; }
.reflog-recovery-card label > div { position: relative; display: flex; align-items: center; }
.reflog-recovery-card label svg { position: absolute; left: 10px; color: var(--color-accent); }
.reflog-recovery-card label input { padding-left: 33px; font-family: var(--font-mono); text-transform: none; letter-spacing: 0; }
.reflog-recovery-card .btn-primary { justify-self: start; }
.new-branch-dialog {
display: block;
width: min(520px, calc(100vw - 32px));
@@ -3124,7 +3302,7 @@
gap: 8px;
padding: 7px 12px;
border-bottom: 1px solid var(--color-border-subtle);
background: #171a2b;
background: var(--code-surface-raised);
font-family: var(--font-mono);
font-size: 12px;
color: var(--color-ink-muted);
@@ -3146,7 +3324,7 @@
font-size: 12px;
line-height: 1.5;
tab-size: 2;
background: #111321;
background: var(--code-surface);
}
.split-pane {
@@ -3170,8 +3348,8 @@
white-space: pre-wrap;
word-break: break-all;
}
.split-span.split-meta { color: var(--color-ink-faint); background: #0c0e18; font-size: 11px; }
.split-span.split-hunk { color: #7aacff; background: rgba(122,172,255,0.08); padding: 3px 10px; }
.split-span.split-meta { color: var(--color-ink-faint); background: var(--code-surface-meta); font-size: 11px; }
.split-span.split-hunk { color: var(--code-hunk-text); background: var(--code-hunk-bg); padding: 3px 10px; }
.split-num {
padding: 0 6px 0 4px;
@@ -3180,11 +3358,11 @@
font-size: 11px;
user-select: none;
border-right: 1px solid var(--color-border-subtle);
background: #0d101a;
background: var(--code-surface-subtle);
}
.split-num.del { background: rgba(232,96,96,0.12); color: rgba(232,96,96,0.6); border-right-color: rgba(232,96,96,0.2); }
.split-num.add { background: rgba(78,202,118,0.1); color: rgba(78,202,118,0.6); border-right-color: rgba(78,202,118,0.2); }
.split-num.empty { background: #10131e; }
.split-num.del { background: var(--code-delete-gutter-bg); color: var(--code-delete-strong); border-right-color: rgba(232,96,96,0.2); }
.split-num.add { background: var(--code-add-gutter-bg); color: var(--code-add-strong); border-right-color: rgba(78,202,118,0.2); }
.split-num.empty { background: var(--code-surface-muted); }
.split-cell {
padding: 0 8px;
@@ -3193,20 +3371,20 @@
min-width: 0;
overflow: visible;
}
.split-cell.del { background: rgba(232,96,96,0.1); color: #ef8080; }
.split-cell.add { background: rgba(78,202,118,0.09); color: #5dd88a; }
.split-cell.empty { background: #10131e; }
.split-cell.del { background: var(--code-delete-bg); color: var(--code-delete-text); }
.split-cell.add { background: var(--code-add-bg); color: var(--code-add-text); }
.split-cell.empty { background: var(--code-surface-muted); }
/* Search-hit highlight: amber, distinct from add (green) / del (red).
Higher specificity so it overrides the add/del backgrounds on a matched line. */
.split-diff .split-cell.match {
background: rgba(240,182,72,0.22);
color: #f3c969;
background: var(--code-match-bg);
color: var(--code-match-text);
box-shadow: inset 2px 0 0 rgba(240,182,72,0.9);
}
.split-diff .split-num.match {
background: rgba(240,182,72,0.2);
color: rgba(240,182,72,0.9);
background: var(--code-match-gutter-bg);
color: var(--code-match-text);
border-right-color: rgba(240,182,72,0.35);
}
@@ -3226,7 +3404,7 @@
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-ink-faint);
background: #0d101a;
background: var(--code-surface-subtle);
}
.split-col-label + .split-col-label { border-left: 1px solid var(--color-border-subtle); }
.split-col-hash {
@@ -3338,7 +3516,7 @@
.line-patch-scroll {
min-height: 0;
overflow: auto;
background: #0b0b14;
background: var(--code-surface);
}
.line-patch-hunk {
@@ -3358,7 +3536,7 @@
min-width: 100%;
padding: 7px 10px;
border-bottom: 1px solid var(--color-border-subtle);
background: rgba(20, 22, 36, 0.96);
background: color-mix(in srgb, var(--code-surface-raised) 96%, transparent);
}
.line-patch-hunk-head code {
color: var(--color-accent);
@@ -3378,23 +3556,23 @@
padding: 0 8px;
border: 1px solid var(--color-border-subtle);
border-radius: 3px;
background: rgba(255, 255, 255, 0.03);
background: var(--code-surface-subtle);
color: var(--color-ink);
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.line-patch-hunk-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
background: var(--code-hover-bg);
}
.line-patch-hunk-button.discard {
border-color: rgba(255, 90, 103, 0.7);
color: #ffccd1;
color: var(--code-delete-text);
}
.line-patch-hunk-button.stage,
.line-patch-hunk-button.unstage {
border-color: rgba(78, 202, 118, 0.72);
color: #bff1ce;
color: var(--code-add-text);
}
.line-patch-lines {
@@ -3412,12 +3590,12 @@
color: var(--color-ink-muted);
}
.line-patch-row.add {
background: rgba(78, 202, 118, 0.09);
color: #bff1ce;
background: var(--code-add-bg);
color: var(--code-add-text);
}
.line-patch-row.delete {
background: rgba(255, 90, 103, 0.1);
color: #ffccd1;
background: var(--code-delete-bg);
color: var(--code-delete-text);
}
.line-patch-row.meta {
color: var(--color-ink-faint);
@@ -3427,8 +3605,8 @@
text-align: center;
user-select: none;
}
.line-patch-row.add .line-patch-prefix { color: #4eca76; }
.line-patch-row.delete .line-patch-prefix { color: #ff6b7a; }
.line-patch-row.add .line-patch-prefix { color: var(--code-add-strong); }
.line-patch-row.delete .line-patch-prefix { color: var(--code-delete-strong); }
.line-patch-row code {
white-space: pre;
font-family: var(--font-mono);
@@ -3437,7 +3615,7 @@
.blame-body {
min-height: 0;
overflow: hidden;
background: #111321;
background: var(--code-surface);
}
.blame-code-header strong {
@@ -3455,7 +3633,7 @@
min-height: 38px;
padding: 6px 10px;
border-bottom: 1px solid var(--color-border-subtle);
background: #111321;
background: var(--code-surface);
}
.blame-search-bar svg {
position: absolute;
@@ -3469,7 +3647,7 @@
padding: 0 34px;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
background: #0b0e18;
background: var(--code-input-bg);
color: var(--color-ink);
font-size: 12px;
}
@@ -3522,7 +3700,7 @@
border-bottom: 1px solid var(--color-border-subtle);
}
.blame-group.uncommitted {
background: #171725;
background: var(--code-surface-raised);
}
.blame-meta {
@@ -3535,8 +3713,8 @@
min-width: 0;
padding: 8px 12px;
border-right: 1px solid var(--color-border-subtle);
background: #0d101a;
box-shadow: 8px 0 18px rgba(0, 0, 0, 0.18);
background: var(--code-surface-subtle);
box-shadow: 8px 0 18px color-mix(in srgb, var(--color-ink) 8%, transparent);
}
.blame-hash {
align-self: flex-start;
@@ -3545,7 +3723,7 @@
padding: 2px 7px;
border: 1px solid rgba(90,140,248,0.2);
border-radius: 5px;
background: #141b2d;
background: var(--code-surface-raised);
color: var(--color-accent);
font-size: 10.5px;
font-weight: 700;
@@ -3562,7 +3740,7 @@
}
.blame-summary {
overflow: hidden;
color: #aeb6d8;
color: var(--color-ink-muted);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
@@ -3572,7 +3750,7 @@
font-size: 10.5px;
}
.blame-group:hover .blame-meta {
background: #111728;
background: var(--code-hover-bg);
}
.blame-group.uncommitted .blame-hash,
.blame-group.uncommitted .blame-author {
@@ -3580,7 +3758,7 @@
}
.blame-group.uncommitted .blame-hash {
border-color: rgba(232, 180, 90, 0.26);
background: #271f14;
background: color-mix(in srgb, #e8b45a 13%, var(--code-surface));
}
.blame-lines {
@@ -3595,16 +3773,16 @@
min-height: 20px;
}
.blame-group:hover .blame-line-number {
background: #111728;
background: var(--code-hover-bg);
}
.blame-group:hover .blame-line-code {
background: #151a2b;
background: var(--code-hover-bg);
}
.blame-search-hit {
padding: 0 1px;
border-radius: 3px;
background: rgba(240,182,72,0.28);
color: #f3d487;
background: var(--code-match-bg);
color: var(--code-match-text);
}
.global-search-body {
@@ -3800,8 +3978,9 @@
margin: 0;
padding: 8px 10px;
border-radius: 7px;
color: #5dd88a;
background: rgba(78,202,118,0.08);
border: 1px solid color-mix(in srgb, var(--code-add-strong) 18%, transparent);
color: var(--code-add-text);
background: var(--code-add-bg);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.45;
@@ -4321,14 +4500,14 @@
}
.diff-line { display: block; white-space: pre-wrap; word-break: break-word; }
.diff-line.meta { color: var(--color-ink-faint); }
.diff-line.hunk { color: #7aacff; background: rgba(122,172,255,0.07); }
.diff-line.add { color: #4eca76; background: rgba(78,202,118,0.09); }
.diff-line.del { color: #e86060; background: rgba(232,96,96,0.09); }
.diff-line.hunk { color: var(--code-hunk-text); background: var(--code-hunk-bg); }
.diff-line.add { color: var(--code-add-text); background: var(--code-add-bg); }
.diff-line.del { color: var(--code-delete-text); background: var(--code-delete-bg); }
.diff-line.context { color: var(--color-ink-muted); }
.diff-counts { display: flex; gap: 8px; font-family: var(--font-mono); font-size: 12px; font-weight: 700; }
.diff-counts .adds { color: #4eca76; }
.diff-counts .dels { color: #e86060; }
.diff-counts .adds { color: var(--code-add-strong); }
.diff-counts .dels { color: var(--code-delete-strong); }
/* --- Conflict resolver --- */
@@ -4399,7 +4578,7 @@
overflow: hidden;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
background: rgba(0,0,0,0.12);
background: var(--code-surface);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
@@ -4431,19 +4610,19 @@
font-weight: 800;
text-transform: uppercase;
}
.resolve-split-marker.unresolved { color: #ef8080; background: rgba(232,96,96,0.12); }
.resolve-split-marker.unresolved { color: var(--code-delete-text); background: var(--code-delete-gutter-bg); }
.resolve-num {
padding: 0 6px 0 4px;
border-right: 1px solid var(--color-border-subtle);
color: var(--color-ink-faint);
background: rgba(0,0,0,0.14);
background: var(--code-surface-subtle);
text-align: right;
user-select: none;
}
.resolve-num.ours { color: rgba(78,202,118,0.65); background: rgba(78,202,118,0.11); border-right-color: rgba(78,202,118,0.2); }
.resolve-num.theirs { color: rgba(122,172,255,0.65); background: rgba(122,172,255,0.11); border-right-color: rgba(122,172,255,0.2); }
.resolve-num.empty { background: rgba(0,0,0,0.07); }
.resolve-num.ours { color: var(--code-add-strong); background: var(--code-add-gutter-bg); border-right-color: rgba(78,202,118,0.2); }
.resolve-num.theirs { color: var(--code-hunk-text); background: var(--code-hunk-bg); border-right-color: rgba(122,172,255,0.2); }
.resolve-num.empty { background: var(--code-surface-muted); }
.resolve-cell {
min-width: 0;
@@ -4452,9 +4631,9 @@
color: var(--color-ink-muted);
white-space: pre;
}
.resolve-cell.ours { color: #5dd88a; background: rgba(78,202,118,0.1); }
.resolve-cell.theirs { color: #8fb4ff; background: rgba(90,140,248,0.11); }
.resolve-cell.empty { background: rgba(0,0,0,0.06); }
.resolve-cell.ours { color: var(--code-add-text); background: var(--code-add-bg); }
.resolve-cell.theirs { color: var(--code-hunk-text); background: var(--code-hunk-bg); }
.resolve-cell.empty { background: var(--code-surface-muted); }
.resolve-cell.dimmed { opacity: 0.42; filter: grayscale(0.5); }
.resolve-context { margin: 0; padding: 2px 8px; overflow-x: auto; font-family: var(--font-mono); font-size: 12px; line-height: 1.5; tab-size: 2; color: var(--color-ink-muted); }
@@ -4474,13 +4653,13 @@
.resolve-side.dimmed { opacity: 0.4; filter: grayscale(0.5); }
.resolve-side-label { font-size: 11px; font-weight: 800; text-transform: uppercase; }
.resolve-side.ours .resolve-side-label { color: #4eca76; }
.resolve-side.theirs .resolve-side-label { color: #6a9aff; }
.resolve-side.ours .resolve-side-label { color: var(--code-add-strong); }
.resolve-side.theirs .resolve-side-label { color: var(--code-hunk-text); }
.resolve-lines { margin: 0; overflow-x: auto; font-family: var(--font-mono); font-size: 12px; line-height: 1.5; tab-size: 2; }
.resolve-line { display: block; white-space: pre-wrap; word-break: break-word; }
.resolve-line.ours { color: #4eca76; }
.resolve-line.theirs { color: #7aacff; }
.resolve-line.ours { color: var(--code-add-text); }
.resolve-line.theirs { color: var(--code-hunk-text); }
.resolve-line.context { color: var(--color-ink-muted); }
.resolve-binary { display: grid; align-content: start; gap: 12px; padding: 4px; }
@@ -4834,22 +5013,75 @@
background: var(--color-surface-hover);
}
:root[data-theme="light"] .split-span.split-meta {
background: #e9eef7;
}
:root[data-theme="light"] .resolve-split,
:root[data-theme="light"] .resolve-num,
:root[data-theme="light"] .resolve-cell.empty,
:root[data-theme="light"] .resolve-num.empty {
background: rgba(234,239,248,0.72);
}
:root[data-theme="light"] .diff-line.meta,
:root[data-theme="light"] .resolve-split-marker {
color: #64728a;
}
:root[data-theme="light"] .global-search-tabs,
:root[data-theme="light"] .file-search-history,
:root[data-theme="light"] .file-search-history-head {
background: var(--color-surface-dim);
}
:root[data-theme="light"] .discard-target {
background: var(--code-surface-subtle);
}
:root[data-theme="light"] .commit-amend-toggle input {
border-color: var(--color-border-input);
background: var(--app-input-bg);
}
:root[data-theme="light"] .commit-amend-toggle input:checked {
border-color: var(--color-primary);
box-shadow: inset 0 0 0 2px #ffffff;
}
:root[data-theme="light"] .cred-segment {
border-color: rgba(49,95,214,0.2);
background: #eef3f9;
}
:root[data-theme="light"] .cred-seg-btn.active {
border-color: rgba(49,95,214,0.26);
background: linear-gradient(135deg, rgba(49,95,214,0.12), rgba(15,143,181,0.08));
box-shadow: 0 8px 18px rgba(28,44,74,0.1), inset 0 1px 0 rgba(255,255,255,0.9);
}
:root[data-theme="light"] .cred-close {
border-color: var(--color-border-subtle);
color: var(--color-ink-dim);
background: rgba(255,255,255,0.76);
}
:root[data-theme="light"] .cred-close:hover:not(:disabled) {
border-color: var(--color-border-input);
color: var(--color-ink);
background: var(--color-surface-hover);
}
:root[data-theme="light"] .status-badge.modified,
:root[data-theme="light"] .resolve-status,
:root[data-theme="light"] .resolve-conflict-label {
color: #8a580a;
}
:root[data-theme="light"] .status-badge.added,
:root[data-theme="light"] .status-badge.untracked,
:root[data-theme="light"] .pill-active,
:root[data-theme="light"] .prepared-tag {
color: #19723d;
}
:root[data-theme="light"] .status-badge.deleted {
color: #b4233b;
}
:root[data-theme="light"] .status-badge.renamed {
color: #245cc7;
}
:root[data-theme="light"] .cred-input input,
:root[data-theme="light"] .cred-expiry input[type="date"] {
background: rgba(255,255,255,0.92);
@@ -5022,6 +5254,10 @@
.file-search-hit { grid-template-columns: auto minmax(0, 1fr); align-items: start; }
.file-search-hit .status-badge,
.file-search-action { grid-column: 2; justify-self: start; }
.rebase-base-bar { grid-template-columns: 1fr; }
.rebase-plan-row { grid-template-columns: auto 96px 54px minmax(180px, 1fr); }
.reflog-body { grid-template-columns: 1fr; grid-template-rows: minmax(220px, 0.8fr) minmax(0, 1.2fr); }
.reflog-list-pane { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
.dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
.branch-actions { flex-direction: row; justify-content: flex-start; }
.tb-action-label { display: none; }
+42 -12
View File
@@ -2,7 +2,7 @@
import { onDestroy, onMount } from "svelte";
import { getVersion } from "@tauri-apps/api/app";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, History, ListRestart, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
import iconUrl from "../../src-tauri/icons/icon.png";
export let branch: string = "";
@@ -20,6 +20,8 @@
export let onRefresh: () => void = () => {};
export let onSearch: () => void = () => {};
export let onCompare: () => void = () => {};
export let onInteractiveRebase: () => void = () => {};
export let onReflog: () => void = () => {};
export let onOpenInExplorer: () => void = () => {};
export let onToggleAutoRefresh: () => void = () => {};
export let onOpenSettings: () => void = () => {};
@@ -79,17 +81,23 @@
<!-- Center: repo + branch info -->
<div class="titlebar-info" data-tauri-drag-region>
{#if hasRepository}
{#if repoName}
<span class="tb-repo" data-tauri-drag-region>{repoName}</span>
<span class="tb-sep" data-tauri-drag-region aria-hidden="true">/</span>
{/if}
<GitBranch size={12} aria-hidden="true" />
<span class="tb-branch" data-tauri-drag-region>{branch}</span>
{#if ahead > 0}
<span class="tb-sync ahead" title="{ahead} commits ahead">{ahead}</span>
{/if}
{#if behind > 0}
<span class="tb-sync behind" title="{behind} commits behind">{behind}</span>
<div class="titlebar-context" data-tauri-drag-region>
{#if repoName}
<span class="tb-repo" data-tauri-drag-region>{repoName}</span>
<span class="tb-sep" data-tauri-drag-region aria-hidden="true">/</span>
{/if}
<GitBranch size={12} aria-hidden="true" />
<span class="tb-branch" data-tauri-drag-region title={branch}>{branch}</span>
</div>
{#if ahead > 0 || behind > 0}
<div class="tb-sync-group" aria-label="Branch synchronization status">
{#if ahead > 0}
<span class="tb-sync ahead" title="{ahead} commits ahead">{ahead}</span>
{/if}
{#if behind > 0}
<span class="tb-sync behind" title="{behind} commits behind">{behind}</span>
{/if}
</div>
{/if}
{:else}
<span class="tb-no-repo" data-tauri-drag-region>No repository open</span>
@@ -132,6 +140,28 @@
<span class="tb-action-label">Compare</span>
</button>
<button
class="tb-action"
onclick={onInteractiveRebase}
disabled={!hasRepository || isBusy}
title="Interactive rebase"
aria-label="Interactive rebase"
>
<ListRestart size={14} aria-hidden="true" />
<span class="tb-action-label">Rebase</span>
</button>
<button
class="tb-action"
onclick={onReflog}
disabled={!hasRepository || isBusy}
title="Reflog"
aria-label="Reflog"
>
<History size={14} aria-hidden="true" />
<span class="tb-action-label">Reflog</span>
</button>
<button
class="tb-action"
onclick={onFetch}
@@ -0,0 +1,148 @@
<script lang="ts">
import { AlertTriangle, ArrowDown, ArrowUp, GitBranch, LoaderCircle, Play, X } from "@lucide/svelte";
import type { GitBranch as GitBranchInfo, RebaseAction, RebaseCommit, RebasePlanItem } from "../types";
interface PlanRow extends RebaseCommit {
action: RebaseAction;
message: string;
}
interface Props {
branches: GitBranchInfo[];
currentBranch: string;
base: string;
commits: RebaseCommit[];
isLoading: boolean;
isBusy: boolean;
operation: string;
error: string;
onBaseChange: (base: string) => void;
onStart: (plan: RebasePlanItem[]) => void;
onClose: () => void;
}
let {
branches = [], currentBranch = "", base = "", commits = [], isLoading = false,
isBusy = false, operation = "", error = "", onBaseChange = () => {},
onStart = () => {}, onClose = () => {},
}: Props = $props();
let rows = $state<PlanRow[]>([]);
$effect(() => {
rows = commits.map((commit) => ({ ...commit, action: "pick", message: commit.summary }));
});
let availableBases = $derived(branches.filter((branch) => !branch.current));
let keptCount = $derived(rows.filter((row) => row.action !== "drop").length);
let invalidSquash = $derived(rows.some((row, index) =>
(row.action === "squash" || row.action === "fixup")
&& rows.slice(0, index).every((previous) => previous.action === "drop")
));
let invalidReword = $derived(rows.some((row) => row.action === "reword" && !row.message.trim()));
let canStart = $derived(Boolean(base) && rows.length > 0 && keptCount > 0 && !invalidSquash && !invalidReword && !isLoading && !isBusy);
function updateAction(index: number, action: RebaseAction) {
rows = rows.map((row, rowIndex) => rowIndex === index ? { ...row, action } : row);
}
function updateMessage(index: number, message: string) {
rows = rows.map((row, rowIndex) => rowIndex === index ? { ...row, message } : row);
}
function move(index: number, direction: -1 | 1) {
const target = index + direction;
if (target < 0 || target >= rows.length) return;
const next = [...rows];
[next[index], next[target]] = [next[target], next[index]];
rows = next;
}
function start() {
if (!canStart) return;
onStart(rows.map((row) => ({
hash: row.hash,
action: row.action,
message: row.action === "reword" ? row.message.trim() : null,
})));
}
</script>
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
<div class="dialog interactive-rebase-dialog" role="dialog" aria-modal="true" aria-label="Interactive rebase" tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Rewrite local history</span>
<h2 class="dialog-title">Interactive rebase</h2>
</div>
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title="Close"><X size={18} aria-hidden="true" /></button>
</header>
<div class="interactive-rebase-body">
<section class="rebase-base-bar">
<label>
<span>Rebase <strong>{currentBranch || "current branch"}</strong> onto</span>
<select value={base} onchange={(event) => onBaseChange((event.target as HTMLSelectElement).value)} disabled={isBusy || isLoading}>
<option value="" disabled>Select a base branch</option>
{#each availableBases as branch (branch.name)}
<option value={branch.name}>{branch.remote ? "Remote · " : "Local · "}{branch.name}</option>
{/each}
</select>
</label>
<p>Oldest commit first. Reorder commits, then choose how each one should be replayed.</p>
</section>
{#if error}
<div class="rebase-warning error"><AlertTriangle size={16} aria-hidden="true" /><span>{error}</span></div>
{/if}
{#if isLoading}
<div class="blank-state"><LoaderCircle class="spin" size={18} aria-hidden="true" /> Loading rebase range…</div>
{:else if !base}
<div class="blank-state">Select the branch or commit that should become the new base.</div>
{:else if rows.length === 0}
<div class="blank-state">No linear commits are available above this base.</div>
{:else}
<div class="rebase-plan" role="list" aria-label="Interactive rebase plan">
{#each rows as row, index (row.hash)}
<article class:drop={row.action === "drop"} class="rebase-plan-row" role="listitem">
<div class="rebase-order-actions">
<button type="button" onclick={() => move(index, -1)} disabled={isBusy || index === 0} title="Move up"><ArrowUp size={14} aria-hidden="true" /></button>
<button type="button" onclick={() => move(index, 1)} disabled={isBusy || index === rows.length - 1} title="Move down"><ArrowDown size={14} aria-hidden="true" /></button>
</div>
<select class={`rebase-action ${row.action}`} value={row.action} onchange={(event) => updateAction(index, (event.target as HTMLSelectElement).value as RebaseAction)} disabled={isBusy} aria-label={`Action for ${row.short_hash}`}>
<option value="pick">pick</option><option value="reword">reword</option><option value="squash">squash</option><option value="fixup">fixup</option><option value="drop">drop</option>
</select>
<code>{row.short_hash}</code>
<div class="rebase-commit-copy">
{#if row.action === "reword"}
<input value={row.message} oninput={(event) => updateMessage(index, (event.target as HTMLInputElement).value)} disabled={isBusy} aria-label={`New message for ${row.short_hash}`} maxlength="240" />
{:else}
<strong>{row.summary}</strong>
{/if}
<span>{row.author_name} · {new Date(row.date).toLocaleString()}</span>
</div>
</article>
{/each}
</div>
{/if}
{#if invalidSquash}
<div class="rebase-warning"><AlertTriangle size={15} aria-hidden="true" /> Squash and fixup need an earlier commit that is not dropped.</div>
{:else if invalidReword}
<div class="rebase-warning"><AlertTriangle size={15} aria-hidden="true" /> Reword messages cannot be empty.</div>
{/if}
</div>
<footer class="dialog-footer">
<span class="dialog-footer-info">{keptCount} of {rows.length} commits kept</span>
<div class="rebase-footer-actions">
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>Cancel</button>
<button class="btn-primary" type="button" onclick={start} disabled={!canStart}>
{#if operation === "Starting interactive rebase"}<LoaderCircle class="spin" size={16} aria-hidden="true" />{:else}<Play size={16} aria-hidden="true" />{/if}
Start rebase
</button>
</div>
</footer>
</div>
</div>
+80
View File
@@ -0,0 +1,80 @@
<script lang="ts">
import { GitBranch, History, LoaderCircle, Search, ShieldCheck, X } from "@lucide/svelte";
import type { ReflogEntry } from "../types";
interface Props {
entries: ReflogEntry[];
currentHash: string;
isLoading: boolean;
isBusy: boolean;
operation: string;
error: string;
onPreview: (entry: ReflogEntry) => void;
onRestore: (entry: ReflogEntry, branch: string) => void;
onClose: () => void;
}
let { entries = [], currentHash = "", isLoading = false, isBusy = false, operation = "", error = "", onPreview = () => {}, onRestore = () => {}, onClose = () => {} }: Props = $props();
let query = $state("");
let selectedHash = $state("");
let recoveryBranch = $state("");
let filteredEntries = $derived(entries.filter((entry) => `${entry.selector} ${entry.action} ${entry.short_hash} ${entry.author_name}`.toLowerCase().includes(query.trim().toLowerCase())));
let selected = $derived(entries.find((entry) => entry.hash === selectedHash) ?? filteredEntries[0] ?? null);
$effect(() => {
if (!selectedHash && entries.length > 0) select(entries[0]);
});
function select(entry: ReflogEntry) {
selectedHash = entry.hash;
recoveryBranch = `recovery/${entry.short_hash}`;
}
</script>
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
<div class="dialog reflog-dialog" role="dialog" aria-modal="true" aria-label="Reflog" tabindex="-1">
<header class="dialog-header">
<div><span class="eyebrow">Recovery history</span><h2 class="dialog-title">Reflog</h2></div>
<button class="dialog-close" type="button" onclick={onClose} disabled={isBusy} title="Close"><X size={18} aria-hidden="true" /></button>
</header>
<div class="reflog-body">
<aside class="reflog-list-pane">
<label class="reflog-search"><Search size={15} aria-hidden="true" /><input bind:value={query} placeholder="Search actions, hashes or authors" aria-label="Search reflog" /></label>
{#if isLoading}
<div class="blank-state"><LoaderCircle class="spin" size={18} aria-hidden="true" /> Loading reflog…</div>
{:else if filteredEntries.length === 0}
<div class="blank-state">No reflog entries match this search.</div>
{:else}
<div class="reflog-list" role="listbox" aria-label="Reflog entries">
{#each filteredEntries as entry (`${entry.selector}:${entry.hash}`)}
<button class:active={selected?.selector === entry.selector} type="button" role="option" aria-selected={selected?.selector === entry.selector} onclick={() => select(entry)}>
<span class="reflog-row-top"><code>{entry.selector}</code><span>{new Date(entry.date).toLocaleString()}</span></span>
<strong>{entry.action}</strong>
<span class="reflog-row-bottom"><code>{entry.short_hash}</code><span>{entry.author_name}</span></span>
</button>
{/each}
</div>
{/if}
</aside>
<section class="reflog-detail">
{#if error}<div class="rebase-warning error">{error}</div>{/if}
{#if selected}
<div class="reflog-detail-head"><History size={20} aria-hidden="true" /><div><span class="eyebrow">{selected.selector}</span><h3>{selected.action}</h3></div></div>
<dl><div><dt>Commit</dt><dd><code>{selected.hash}</code></dd></div><div><dt>Author</dt><dd>{selected.author_name}</dd></div><div><dt>Date</dt><dd>{new Date(selected.date).toLocaleString()}</dd></div></dl>
<button class="btn-secondary reflog-preview" type="button" onclick={() => onPreview(selected)} disabled={isBusy || selected.hash === currentHash}><History size={15} aria-hidden="true" /> Preview changes to current HEAD</button>
<div class="reflog-recovery-card">
<div class="reflog-recovery-title"><ShieldCheck size={18} aria-hidden="true" /><div><strong>Safe recovery</strong><span>Create a new branch here. The current branch is not reset or deleted.</span></div></div>
<label><span>Recovery branch</span><div><GitBranch size={15} aria-hidden="true" /><input bind:value={recoveryBranch} disabled={isBusy} spellcheck="false" /></div></label>
<button class="btn-primary" type="button" onclick={() => onRestore(selected, recoveryBranch.trim())} disabled={isBusy || !recoveryBranch.trim()}>
{#if operation === "Restoring reflog entry"}<LoaderCircle class="spin" size={16} aria-hidden="true" />{:else}<ShieldCheck size={16} aria-hidden="true" />{/if}
Create and checkout recovery branch
</button>
</div>
{:else}
<div class="blank-state">Select a reflog entry to inspect or recover it.</div>
{/if}
</section>
</div>
</div>
</div>
+23
View File
@@ -10,6 +10,9 @@ import type {
GitCommit,
GitCommitComparison,
GitRepositoryFile,
RebaseCommit,
RebasePlanItem,
ReflogEntry,
GitSearchHit,
GitStash,
GitStatus,
@@ -306,6 +309,26 @@ export function rebaseAbort(path: string): Promise<GitStatus> {
return invoke<GitStatus>("rebase_abort", { path });
}
export function listInteractiveRebaseCommits(path: string, base: string): Promise<RebaseCommit[]> {
return invoke<RebaseCommit[]>("list_interactive_rebase_commits", { path, base });
}
export function startInteractiveRebase(
path: string,
base: string,
plan: RebasePlanItem[],
): Promise<GitStatus> {
return invoke<GitStatus>("start_interactive_rebase", { path, base, plan });
}
export function listReflog(path: string, limit = 250): Promise<ReflogEntry[]> {
return invoke<ReflogEntry[]>("list_reflog", { path, limit });
}
export function restoreReflogEntry(path: string, commit: string, branch: string): Promise<GitStatus> {
return invoke<GitStatus>("restore_reflog_entry", { path, commit, branch });
}
export function listRepositoryFiles(path: string): Promise<GitRepositoryFile[]> {
return invoke<GitRepositoryFile[]>("list_repository_files", { path });
}
+25
View File
@@ -198,6 +198,31 @@ export interface GitBlameResult {
lines: GitBlameLine[];
}
export type RebaseAction = "pick" | "reword" | "squash" | "fixup" | "drop";
export interface RebaseCommit {
hash: string;
short_hash: string;
summary: string;
author_name: string;
date: string;
}
export interface RebasePlanItem {
hash: string;
action: RebaseAction;
message: string | null;
}
export interface ReflogEntry {
hash: string;
short_hash: string;
selector: string;
action: string;
author_name: string;
date: string;
}
export interface StoredCredential {
username: string;
password: string;