feat(commits): enhance commit listing with pagination and skipping
This update introduces pagination and skipping functionality for the commit listing feature, allowing users to load commits in pages and navigate through them more efficiently. The UI has been adjusted to support loading more commits dynamically, improving the overall user experience when dealing with large repositories. - Added pagination support for commit history - Introduced a loading mechanism for fetching more commits - Updated UI components to reflect changes in commit loading behavior
This commit is contained in:
+170
-109
@@ -8,37 +8,28 @@
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||
import RepoTabs from "./lib/RepoTabs.svelte";
|
||||
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
||||
import AiReviewDialog from "./lib/components/AiReviewDialog.svelte";
|
||||
import AiCommitSplitDialog from "./lib/components/AiCommitSplitDialog.svelte";
|
||||
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
|
||||
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
||||
import BlameDialog from "./lib/components/BlameDialog.svelte";
|
||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
|
||||
import CommitPanel from "./lib/components/CommitPanel.svelte";
|
||||
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
||||
import CompareSelectDialog from "./lib/components/CompareSelectDialog.svelte";
|
||||
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
||||
import DiscardConfirmDialog from "./lib/components/DiscardConfirmDialog.svelte";
|
||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||
import HelpOverlay from "./lib/components/HelpOverlay.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";
|
||||
import StatusPanel from "./lib/components/StatusPanel.svelte";
|
||||
import SyncSettingsDialog from "./lib/components/SyncSettingsDialog.svelte";
|
||||
import UpdateToast from "./lib/components/UpdateToast.svelte";
|
||||
import WorktreeDialog from "./lib/components/WorktreeDialog.svelte";
|
||||
|
||||
import {
|
||||
amendCommit,
|
||||
@@ -250,6 +241,7 @@
|
||||
const FILE_HISTORY_MIN_WIDTH = 240;
|
||||
const FILE_HISTORY_MAX_WIDTH = 520;
|
||||
const ERROR_AUTO_HIDE_MS = 6000;
|
||||
const COMMIT_HISTORY_PAGE_SIZE = 50;
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -279,6 +271,10 @@
|
||||
let tags: GitTag[] = [];
|
||||
let stashes: GitStash[] = [];
|
||||
let commits: GitCommit[] = [];
|
||||
let commitHistoryHasMore = false;
|
||||
let commitHistoryLoadingMore = false;
|
||||
let commitHistoryLoadError = "";
|
||||
let commitHistoryRequestId = 0;
|
||||
let repoFiles: GitRepositoryFile[] = [];
|
||||
let selectedExplorerPath = "";
|
||||
let selectedExplorerKind: ExplorerNodeKind = "file";
|
||||
@@ -754,7 +750,7 @@
|
||||
if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
|
||||
applyStatus(nextStatus);
|
||||
// Something changed — reload branches, commits and files in one bundled call.
|
||||
const bundle = await openRepositoryBundle(path, 100);
|
||||
const bundle = await openRepositoryBundle(path, Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length) + 1);
|
||||
if (!sameRepoPath(path, activeRepoPath)) return;
|
||||
const previousHeadHash = lastFileHistoryHeadHash;
|
||||
await refreshBranchList(path, bundle.branches);
|
||||
@@ -1826,6 +1822,10 @@
|
||||
branches = [];
|
||||
stashes = [];
|
||||
commits = [];
|
||||
commitHistoryHasMore = false;
|
||||
commitHistoryLoadingMore = false;
|
||||
commitHistoryLoadError = "";
|
||||
commitHistoryRequestId += 1;
|
||||
lastFileHistoryHeadHash = "";
|
||||
repoFiles = [];
|
||||
selectedExplorerPath = "";
|
||||
@@ -1945,7 +1945,19 @@
|
||||
}
|
||||
|
||||
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
|
||||
commits = prefetched ?? (await listCommits(path, 100));
|
||||
const targetLimit = prefetched
|
||||
? Math.max(COMMIT_HISTORY_PAGE_SIZE, prefetched.length - 1)
|
||||
: Math.max(COMMIT_HISTORY_PAGE_SIZE, commits.length);
|
||||
const requestId = ++commitHistoryRequestId;
|
||||
commitHistoryLoadingMore = false;
|
||||
commitHistoryLoadError = "";
|
||||
const history = prefetched ?? (await listCommits(path, targetLimit + 1, 0));
|
||||
if (requestId !== commitHistoryRequestId || !sameRepoPath(path, activeRepoPath)) return;
|
||||
|
||||
commits = history.slice(0, targetLimit);
|
||||
commitHistoryHasMore = history.length > targetLimit;
|
||||
commitHistoryLoadingMore = false;
|
||||
commitHistoryLoadError = "";
|
||||
lastFileHistoryHeadHash = commits[0]?.hash ?? "";
|
||||
const hashes = new Set(commits.map((c) => c.hash));
|
||||
if (compareFrom && !hashes.has(compareFrom)) compareFrom = "";
|
||||
@@ -1958,6 +1970,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreCommitHistory() {
|
||||
if (!activeRepoPath || commitHistoryLoadingMore || !commitHistoryHasMore || isBusy) return;
|
||||
|
||||
const path = activeRepoPath;
|
||||
const offset = commits.length;
|
||||
const requestId = ++commitHistoryRequestId;
|
||||
commitHistoryLoadingMore = true;
|
||||
commitHistoryLoadError = "";
|
||||
|
||||
try {
|
||||
const history = await listCommits(path, COMMIT_HISTORY_PAGE_SIZE + 1, offset);
|
||||
if (requestId !== commitHistoryRequestId || !sameRepoPath(path, activeRepoPath)) return;
|
||||
|
||||
const knownHashes = new Set(commits.map((commit) => commit.hash));
|
||||
const nextPage = history
|
||||
.slice(0, COMMIT_HISTORY_PAGE_SIZE)
|
||||
.filter((commit) => !knownHashes.has(commit.hash));
|
||||
commits = [...commits, ...nextPage];
|
||||
commitHistoryHasMore = history.length > COMMIT_HISTORY_PAGE_SIZE;
|
||||
} catch (error) {
|
||||
if (requestId !== commitHistoryRequestId || !sameRepoPath(path, activeRepoPath)) return;
|
||||
commitHistoryLoadError = errorToMessage(error);
|
||||
} finally {
|
||||
if (requestId === commitHistoryRequestId) commitHistoryLoadingMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshExplorerFiles(path = activeRepoPath, prefetched?: GitRepositoryFile[]) {
|
||||
repoFiles = prefetched ?? (await listRepositoryFiles(path));
|
||||
const folderPaths = allExplorerFolderPaths(repoFiles);
|
||||
@@ -2043,7 +2082,7 @@
|
||||
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);
|
||||
const bundle = await openRepositoryBundle(path, COMMIT_HISTORY_PAGE_SIZE + 1);
|
||||
if (requestId !== repoOpenRequestId) return;
|
||||
resetRepositoryState(false);
|
||||
applyStatus(bundle.status);
|
||||
@@ -2117,7 +2156,7 @@
|
||||
directoryName || undefined,
|
||||
username,
|
||||
password,
|
||||
100,
|
||||
COMMIT_HISTORY_PAGE_SIZE + 1,
|
||||
);
|
||||
resetRepositoryState(false);
|
||||
applyStatus(bundle.status);
|
||||
@@ -4572,7 +4611,11 @@
|
||||
repositoryKey={activeRepoPath}
|
||||
{hasRepository}
|
||||
{isBusy}
|
||||
hasMore={commitHistoryHasMore}
|
||||
isLoadingMore={commitHistoryLoadingMore}
|
||||
loadMoreError={commitHistoryLoadError}
|
||||
{expandedCommitHashes}
|
||||
onLoadMore={loadMoreCommitHistory}
|
||||
onRestoreCommit={restoreCommit}
|
||||
onPreviewCommitFile={previewCommitFileFromHistory}
|
||||
onCreateBranchFromCommit={openNewBranchDialog}
|
||||
@@ -4665,7 +4708,9 @@
|
||||
{/if}
|
||||
|
||||
{#if helpOpen}
|
||||
<HelpOverlay language={appLanguage} onClose={() => { helpOpen = false; }} />
|
||||
{#await import("./lib/components/HelpOverlay.svelte") then module}
|
||||
<module.default language={appLanguage} onClose={() => { helpOpen = false; }} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if aiReviewOpen && aiReviewResult}
|
||||
@@ -4688,28 +4733,32 @@
|
||||
{/if}
|
||||
|
||||
{#if linePatchOpen && linePatchFile}
|
||||
<LinePatchDialog
|
||||
file={linePatchFile}
|
||||
staged={linePatchStaged}
|
||||
patch={linePatchText}
|
||||
{isBusy}
|
||||
isLoading={linePatchLoading}
|
||||
error={linePatchError}
|
||||
onClose={closeLinePatch}
|
||||
onRefresh={refreshLinePatch}
|
||||
onApply={applyLinePatch}
|
||||
/>
|
||||
{#await import("./lib/components/LinePatchDialog.svelte") then module}
|
||||
<module.default
|
||||
file={linePatchFile}
|
||||
staged={linePatchStaged}
|
||||
patch={linePatchText}
|
||||
{isBusy}
|
||||
isLoading={linePatchLoading}
|
||||
error={linePatchError}
|
||||
onClose={closeLinePatch}
|
||||
onRefresh={refreshLinePatch}
|
||||
onApply={applyLinePatch}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if blameOpen}
|
||||
<BlameDialog
|
||||
filePath={blameFilePath}
|
||||
lines={blameLines}
|
||||
{isBusy}
|
||||
isLoading={blameLoading}
|
||||
error={blameError}
|
||||
onClose={closeBlame}
|
||||
/>
|
||||
{#await import("./lib/components/BlameDialog.svelte") then module}
|
||||
<module.default
|
||||
filePath={blameFilePath}
|
||||
lines={blameLines}
|
||||
{isBusy}
|
||||
isLoading={blameLoading}
|
||||
error={blameError}
|
||||
onClose={closeBlame}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if pendingDiscard}
|
||||
@@ -4724,44 +4773,48 @@
|
||||
{/if}
|
||||
|
||||
{#if globalSearchOpen}
|
||||
<GlobalSearchDialog
|
||||
{hasRepository}
|
||||
{isBusy}
|
||||
isSearching={globalSearchBusy}
|
||||
error={globalSearchError}
|
||||
results={globalSearchResults}
|
||||
files={repoFiles}
|
||||
fileHistory={fileHistory}
|
||||
selectedFilePath={selectedExplorerPath}
|
||||
onClose={closeGlobalSearchDialog}
|
||||
onSearch={runGlobalSearch}
|
||||
onCancel={cancelGlobalSearch}
|
||||
onDiff={diffSearchHit}
|
||||
onSelectFile={selectFileFromSearch}
|
||||
onFileHistoryDiff={diffSelectedFileFromCommit}
|
||||
onFileHistoryRestore={restoreSelectedFileFromCommit}
|
||||
/>
|
||||
{#await import("./lib/components/GlobalSearchDialog.svelte") then module}
|
||||
<module.default
|
||||
{hasRepository}
|
||||
{isBusy}
|
||||
isSearching={globalSearchBusy}
|
||||
error={globalSearchError}
|
||||
results={globalSearchResults}
|
||||
files={repoFiles}
|
||||
fileHistory={fileHistory}
|
||||
selectedFilePath={selectedExplorerPath}
|
||||
onClose={closeGlobalSearchDialog}
|
||||
onSearch={runGlobalSearch}
|
||||
onCancel={cancelGlobalSearch}
|
||||
onDiff={diffSearchHit}
|
||||
onSelectFile={selectFileFromSearch}
|
||||
onFileHistoryDiff={diffSelectedFileFromCommit}
|
||||
onFileHistoryRestore={restoreSelectedFileFromCommit}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if worktreeDialogOpen}
|
||||
<WorktreeDialog
|
||||
{worktrees}
|
||||
{branches}
|
||||
initialBranch={worktreeInitialBranch}
|
||||
isLoading={worktreesLoading}
|
||||
{isBusy}
|
||||
error={worktreeError}
|
||||
onRefresh={refreshWorktrees}
|
||||
onOpen={openWorktreeTab}
|
||||
onAdd={createWorktree}
|
||||
onRemove={removeSelectedWorktree}
|
||||
onMove={moveSelectedWorktree}
|
||||
onLock={lockSelectedWorktree}
|
||||
onUnlock={unlockSelectedWorktree}
|
||||
onPrune={pruneStaleWorktrees}
|
||||
onRepair={repairSelectedWorktree}
|
||||
onClose={closeWorktreeDialog}
|
||||
/>
|
||||
{#await import("./lib/components/WorktreeDialog.svelte") then module}
|
||||
<module.default
|
||||
{worktrees}
|
||||
{branches}
|
||||
initialBranch={worktreeInitialBranch}
|
||||
isLoading={worktreesLoading}
|
||||
{isBusy}
|
||||
error={worktreeError}
|
||||
onRefresh={refreshWorktrees}
|
||||
onOpen={openWorktreeTab}
|
||||
onAdd={createWorktree}
|
||||
onRemove={removeSelectedWorktree}
|
||||
onMove={moveSelectedWorktree}
|
||||
onLock={lockSelectedWorktree}
|
||||
onUnlock={unlockSelectedWorktree}
|
||||
onPrune={pruneStaleWorktrees}
|
||||
onRepair={repairSelectedWorktree}
|
||||
onClose={closeWorktreeDialog}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<!-- Create a branch from a specific commit in the history -->
|
||||
@@ -4797,29 +4850,33 @@
|
||||
|
||||
<!-- Choose the AI provider/model used to generate commit messages -->
|
||||
{#if aiSettingsOpen}
|
||||
<AiSettingsDialog
|
||||
settings={aiSettings}
|
||||
localModels={localModelOptions}
|
||||
onSave={saveAiSettings}
|
||||
onClose={() => { aiSettingsOpen = false; }}
|
||||
/>
|
||||
{#await import("./lib/components/AiSettingsDialog.svelte") then module}
|
||||
<module.default
|
||||
settings={aiSettings}
|
||||
localModels={localModelOptions}
|
||||
onSave={saveAiSettings}
|
||||
onClose={() => { aiSettingsOpen = false; }}
|
||||
/>
|
||||
{/await}
|
||||
{/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; }}
|
||||
/>
|
||||
{#await import("./lib/components/InteractiveRebaseDialog.svelte") then module}
|
||||
<module.default
|
||||
{branches}
|
||||
currentBranch={status?.current_branch ?? ""}
|
||||
base={interactiveRebaseBase}
|
||||
commits={interactiveRebaseCommits}
|
||||
isLoading={interactiveRebaseLoading}
|
||||
{isBusy}
|
||||
{operation}
|
||||
error={interactiveRebaseError}
|
||||
onBaseChange={loadInteractiveRebaseRange}
|
||||
onStart={runInteractiveRebase}
|
||||
onClose={() => { if (!isBusy) interactiveRebaseOpen = false; }}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if reflogOpen}
|
||||
@@ -4854,16 +4911,18 @@
|
||||
|
||||
<!-- Compare diff dialog (rendered last so it overlays the search dialog when opened from a hit) -->
|
||||
{#if compareDialogOpen && comparison}
|
||||
<CompareDialog
|
||||
{comparison}
|
||||
{selectedDiffPath}
|
||||
{isBusy}
|
||||
highlightQuery={diffHighlightQuery}
|
||||
restoreLabel={pendingRestoreFile ? "Restore file" : ""}
|
||||
onClose={closeCompareDialog}
|
||||
onRestore={restorePreviewedCommitFile}
|
||||
onSelectFile={selectDiffFile}
|
||||
/>
|
||||
{#await import("./lib/components/CompareDialog.svelte") then module}
|
||||
<module.default
|
||||
{comparison}
|
||||
{selectedDiffPath}
|
||||
{isBusy}
|
||||
highlightQuery={diffHighlightQuery}
|
||||
restoreLabel={pendingRestoreFile ? "Restore file" : ""}
|
||||
onClose={closeCompareDialog}
|
||||
onRestore={restorePreviewedCommitFile}
|
||||
onSelectFile={selectDiffFile}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<!-- Credential dialog for push/pull -->
|
||||
@@ -4918,16 +4977,18 @@
|
||||
|
||||
<!-- Conflict resolve dialog -->
|
||||
{#if resolveDialogOpen}
|
||||
<ResolveDialog
|
||||
{conflictedFiles}
|
||||
{conflictTarget}
|
||||
{conflict}
|
||||
{preparedResolutions}
|
||||
{isBusy}
|
||||
{operation}
|
||||
onClose={() => { resolveDialogOpen = false; }}
|
||||
onSelectFile={selectConflictFile}
|
||||
onMarkResolved={handleMarkResolved}
|
||||
onApply={applyPreparedResolutions}
|
||||
/>
|
||||
{#await import("./lib/components/ResolveDialog.svelte") then module}
|
||||
<module.default
|
||||
{conflictedFiles}
|
||||
{conflictTarget}
|
||||
{conflict}
|
||||
{preparedResolutions}
|
||||
{isBusy}
|
||||
{operation}
|
||||
onClose={() => { resolveDialogOpen = false; }}
|
||||
onSelectFile={selectConflictFile}
|
||||
onMarkResolved={handleMarkResolved}
|
||||
onApply={applyPreparedResolutions}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user