Optimize repository loading and enhance Git UI
Consolidate multiple Git backend calls into a single bundle to reduce overhead when opening and auto-refreshing repositories. This significantly improves performance by fetching status, branches, commits, and files in one optimized operation, instead of re-running 'git rev-parse' and 'git status' multiple times. Also, streamline commit history loading by fetching file changes inline via 'git log --name-status -z', eliminating expensive per-commit 'git diff-tree' processes. Additionally, introduce the ability to create new branches from a specific commit in the history and refactor the commit comparison feature into a dedicated dialog. The status panel now displays concise file names.
This commit is contained in:
+89
-39
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||
import { AlertCircle, Check, FolderOpen, GitBranch, GitMerge, LoaderCircle } from "@lucide/svelte";
|
||||
@@ -8,12 +8,13 @@
|
||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
||||
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
||||
import ComparePanel from "./lib/components/ComparePanel.svelte";
|
||||
import CompareSelectDialog from "./lib/components/CompareSelectDialog.svelte";
|
||||
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||
import NewBranchDialog from "./lib/components/NewBranchDialog.svelte";
|
||||
import RepoLoadingOverlay from "./lib/components/RepoLoadingOverlay.svelte";
|
||||
import ResolveDialog from "./lib/components/ResolveDialog.svelte";
|
||||
import StatusPanel from "./lib/components/StatusPanel.svelte";
|
||||
@@ -33,7 +34,7 @@
|
||||
listFileHistory,
|
||||
listRepositoryFiles,
|
||||
mergeBranch,
|
||||
openRepository,
|
||||
openRepositoryBundle,
|
||||
pull,
|
||||
push,
|
||||
getRemoteUrl,
|
||||
@@ -96,6 +97,8 @@
|
||||
let compareFrom = "";
|
||||
let compareTo = "";
|
||||
let comparison: GitCommitComparison | null = null;
|
||||
let newBranchCommit: GitCommit | null = null;
|
||||
let compareSelectOpen = false;
|
||||
let compareDialogOpen = false;
|
||||
let selectedDiffPath = "";
|
||||
let diffHighlightQuery = "";
|
||||
@@ -167,15 +170,18 @@
|
||||
}
|
||||
|
||||
async function autoRefreshTick() {
|
||||
if (!autoRefreshEnabled || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || globalSearchOpen) return;
|
||||
if (!autoRefreshEnabled || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || newBranchCommit || globalSearchOpen) return;
|
||||
autoRefreshInFlight = true;
|
||||
try {
|
||||
// Cheap fast path: only fetch status; skip the heavy reload if nothing changed.
|
||||
const nextStatus = await getStatus(activeRepoPath);
|
||||
if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
|
||||
applyStatus(nextStatus);
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
// Something changed — reload branches, commits and files in one bundled call.
|
||||
const bundle = await openRepositoryBundle(activeRepoPath, 100);
|
||||
await refreshBranchList(activeRepoPath, bundle.branches);
|
||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
} catch { /* ignore transient errors */ } finally {
|
||||
autoRefreshInFlight = false;
|
||||
@@ -321,12 +327,12 @@
|
||||
|
||||
// ── Refresh helpers ────────────────────────────────────────────────────────
|
||||
|
||||
async function refreshBranchList(path = activeRepoPath) {
|
||||
branches = await listBranches(path);
|
||||
async function refreshBranchList(path = activeRepoPath, prefetched?: GitBranchInfo[]) {
|
||||
branches = prefetched ?? (await listBranches(path));
|
||||
}
|
||||
|
||||
async function refreshCommitHistory(path = activeRepoPath) {
|
||||
commits = await listCommits(path, 100);
|
||||
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
|
||||
commits = prefetched ?? (await listCommits(path, 100));
|
||||
const hashes = new Set(commits.map((c) => c.hash));
|
||||
if (compareFrom && !hashes.has(compareFrom)) compareFrom = "";
|
||||
if (compareTo && !hashes.has(compareTo)) compareTo = "";
|
||||
@@ -338,8 +344,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshExplorerFiles(path = activeRepoPath) {
|
||||
repoFiles = await listRepositoryFiles(path);
|
||||
async function refreshExplorerFiles(path = activeRepoPath, prefetched?: GitRepositoryFile[]) {
|
||||
repoFiles = prefetched ?? (await listRepositoryFiles(path));
|
||||
const folderPaths = allExplorerFolderPaths(repoFiles);
|
||||
expandedExplorerPaths = new Set([...expandedExplorerPaths].filter((folder) => folderPaths.has(folder)));
|
||||
if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
|
||||
@@ -361,20 +367,28 @@
|
||||
repoPath = path;
|
||||
|
||||
await runOperation("Opening repository", async () => {
|
||||
const nextStatus = await openRepository(path);
|
||||
applyStatus(nextStatus);
|
||||
// Paint the loading overlay before the (potentially slow) git enumeration
|
||||
// starts — otherwise the first paint is deferred until the bundle resolves
|
||||
// and the overlay appears to "come late".
|
||||
await tick();
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||
// Single backend round-trip: resolves the repo and reads status, branches,
|
||||
// commits and files in one pass instead of four sequential git calls.
|
||||
const bundle = await openRepositoryBundle(path, 100);
|
||||
applyStatus(bundle.status);
|
||||
branches = []; commits = []; repoFiles = [];
|
||||
selectedExplorerPath = ""; selectedExplorerKind = "file";
|
||||
expandedExplorerPaths = new Set(); expandedCommitHashes = new Set();
|
||||
fileHistory = []; compareFrom = ""; compareTo = "";
|
||||
comparison = null; compareDialogOpen = false; selectedDiffPath = ""; pendingRestoreFile = null;
|
||||
comparison = null; compareSelectOpen = false; compareDialogOpen = false; selectedDiffPath = ""; pendingRestoreFile = null;
|
||||
newBranchCommit = null;
|
||||
if (globalSearchBusy) void cancelGlobalSearch();
|
||||
globalSearchResults = []; globalSearchOpen = false; globalSearchError = "";
|
||||
resolveDialogOpen = false; conflictTarget = ""; conflict = null;
|
||||
preparedResolutions = {};
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshBranchList(activeRepoPath, bundle.branches);
|
||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -429,6 +443,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openNewBranchDialog(commit: GitCommit) {
|
||||
if (!activeRepoPath || isBusy) return;
|
||||
newBranchCommit = commit;
|
||||
}
|
||||
|
||||
async function createBranchFromCommit(branchName: string) {
|
||||
const target = newBranchCommit;
|
||||
const name = branchName.trim();
|
||||
if (!activeRepoPath || !target || !name) return;
|
||||
await runOperation(`Creating ${name}`, async () => {
|
||||
applyStatus(await createBranch(activeRepoPath, name, target.hash));
|
||||
newBranchCommit = null;
|
||||
await refreshBranchList(activeRepoPath);
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
});
|
||||
}
|
||||
|
||||
async function merge(branch: GitBranchInfo) {
|
||||
if (!activeRepoPath || branch.current) return;
|
||||
await runOperation(`Merging ${branch.name}`, async () => {
|
||||
@@ -780,6 +813,11 @@
|
||||
|
||||
// ── Compare ────────────────────────────────────────────────────────────────
|
||||
|
||||
function openCompareSelect() {
|
||||
if (!hasRepository) return;
|
||||
compareSelectOpen = true;
|
||||
}
|
||||
|
||||
async function compareSelectedCommits() {
|
||||
if (!canCompare) return;
|
||||
await runOperation("Comparing commits", async () => {
|
||||
@@ -788,6 +826,7 @@
|
||||
selectedDiffPath = result.files[0]?.path ?? "";
|
||||
diffHighlightQuery = "";
|
||||
pendingRestoreFile = null;
|
||||
compareSelectOpen = false;
|
||||
compareDialogOpen = true;
|
||||
});
|
||||
}
|
||||
@@ -816,10 +855,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openCompareDialog() {
|
||||
if (comparison) compareDialogOpen = true;
|
||||
}
|
||||
|
||||
function closeCompareDialog() {
|
||||
compareDialogOpen = false;
|
||||
pendingRestoreFile = null;
|
||||
@@ -947,6 +982,8 @@
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||
else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
|
||||
}
|
||||
</script>
|
||||
@@ -972,6 +1009,7 @@
|
||||
onPush={pushRepo}
|
||||
onRefresh={refreshRepo}
|
||||
onSearch={() => { globalSearchOpen = true; }}
|
||||
onCompare={openCompareSelect}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
/>
|
||||
|
||||
@@ -1063,7 +1101,7 @@
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<!-- Center: summary + status + commit + compare -->
|
||||
<!-- Center: summary + status + commit -->
|
||||
<section class="main-panel" aria-label="Repository status">
|
||||
<div class="repo-summary">
|
||||
<div class="repo-meta">
|
||||
@@ -1106,21 +1144,6 @@
|
||||
onCommitMessageChange={(msg) => { commitMessage = msg; }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ComparePanel
|
||||
{commits}
|
||||
{hasRepository}
|
||||
{isBusy}
|
||||
{compareFrom}
|
||||
{compareTo}
|
||||
{canCompare}
|
||||
{comparison}
|
||||
{operation}
|
||||
onCompareFromChange={(val) => { compareFrom = val; }}
|
||||
onCompareToChange={(val) => { compareTo = val; }}
|
||||
onCompare={compareSelectedCommits}
|
||||
onOpenDialog={openCompareDialog}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- Right sidebar: commit graph + file history -->
|
||||
@@ -1132,6 +1155,7 @@
|
||||
{expandedCommitHashes}
|
||||
onRestoreCommit={restoreCommit}
|
||||
onPreviewCommitFile={previewCommitFileFromHistory}
|
||||
onCreateBranchFromCommit={openNewBranchDialog}
|
||||
onToggleCommitFiles={(hash) => {
|
||||
const next = new Set(expandedCommitHashes);
|
||||
if (next.has(hash)) next.delete(hash); else next.add(hash);
|
||||
@@ -1185,6 +1209,32 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Create a branch from a specific commit in the history -->
|
||||
{#if newBranchCommit}
|
||||
<NewBranchDialog
|
||||
commit={newBranchCommit}
|
||||
{isBusy}
|
||||
onCreate={createBranchFromCommit}
|
||||
onClose={() => { newBranchCommit = null; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Compare: pick the two commits to diff -->
|
||||
{#if compareSelectOpen}
|
||||
<CompareSelectDialog
|
||||
{commits}
|
||||
{compareFrom}
|
||||
{compareTo}
|
||||
{canCompare}
|
||||
{isBusy}
|
||||
{operation}
|
||||
onCompareFromChange={(val) => { compareFrom = val; }}
|
||||
onCompareToChange={(val) => { compareTo = val; }}
|
||||
onCompare={compareSelectedCommits}
|
||||
onClose={() => { compareSelectOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Compare diff dialog (rendered last so it overlays the search dialog when opened from a hit) -->
|
||||
{#if compareDialogOpen && comparison}
|
||||
<CompareDialog
|
||||
|
||||
Reference in New Issue
Block a user