feat: Redesign git changes to split stage/unstaged files. (#1359)
* feat: Redesign git changes to split stage/unstaged files. Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * refactor: streamline git changes panel * fix: label staged and working diff tabs * fix: isolate staged and working diff files * fix: scope staged and working diff updates * fix: scope git row revert to working changes --------- Signed-off-by: Paolo Insogna <paolo@cowtech.it> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
9af0de0056
commit
e16097b05d
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useFireworksCelebration } from '@/contexts/FireworksContext';
|
||||
import type { GitIdentityProfile, CommitFileEntry } from '@/lib/api/types';
|
||||
import type { GitIdentityProfile, CommitFileEntry, GitStatus } from '@/lib/api/types';
|
||||
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
@@ -47,7 +47,7 @@ import { IntegrateCommitsSection } from './git/IntegrateCommitsSection';
|
||||
|
||||
import { GitHeader } from './git/GitHeader';
|
||||
import { StashesDialog } from './git/StashesDialog';
|
||||
import { ChangesSection } from './git/ChangesSection';
|
||||
import { ChangesPanel, type ChangesGroupConfig } from './git/ChangesPanel';
|
||||
import { CommitSection } from './git/CommitSection';
|
||||
import { GitEmptyState } from './git/GitEmptyState';
|
||||
import { HistorySection } from './git/HistorySection';
|
||||
@@ -56,6 +56,7 @@ import { ConflictDialog } from './git/ConflictDialog';
|
||||
import { StashDialog } from './git/StashDialog';
|
||||
import { InProgressOperationBanner } from './git/InProgressOperationBanner';
|
||||
import { BranchIntegrationSection, type OperationLogEntry } from './git/BranchIntegrationSection';
|
||||
import { createGitIndexMutationQueue, type GitIndexMutationDirection, type GitIndexMutationQueue } from './git/gitIndexMutationQueue';
|
||||
import type { GitRemote } from '@/lib/gitApi';
|
||||
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -74,13 +75,13 @@ type HistoryBranchDivider = {
|
||||
} | null;
|
||||
|
||||
const GIT_ACTION_TAB_STORAGE_KEY = 'oc.git.actionTab';
|
||||
const GIT_RECONCILE_DELAY_MS = 15000;
|
||||
|
||||
const isActionTab = (value: unknown): value is ActionTab =>
|
||||
value === 'commit' || value === 'branch' || value === 'pr';
|
||||
|
||||
type GitViewSnapshot = {
|
||||
directory?: string;
|
||||
selectedPaths: string[];
|
||||
commitMessage: string;
|
||||
generatedHighlights: string[];
|
||||
};
|
||||
@@ -220,6 +221,17 @@ const gitViewSnapshots = new Map<string, GitViewSnapshot>();
|
||||
const normalizePath = (value?: string | null): string =>
|
||||
(value || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
|
||||
const isStagedStatusFile = (file: GitStatus['files'][number]): boolean => {
|
||||
const indexStatus = file.index?.trim();
|
||||
return Boolean(indexStatus && indexStatus !== '?');
|
||||
};
|
||||
|
||||
const isUnstagedStatusFile = (file: GitStatus['files'][number]): boolean => {
|
||||
const workingStatus = file.working_dir?.trim();
|
||||
const indexStatus = file.index?.trim();
|
||||
return Boolean(workingStatus || indexStatus === '?');
|
||||
};
|
||||
|
||||
export const GitView: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const { git } = useRuntimeAPIs();
|
||||
@@ -304,11 +316,116 @@ export const GitView: React.FC = () => {
|
||||
const fetchIdentity = useGitStore((state) => state.fetchIdentity);
|
||||
const prefetchDiffs = useGitStore((state) => state.prefetchDiffs);
|
||||
const setLogMaxCount = useGitStore((state) => state.setLogMaxCount);
|
||||
const moveStatusPathsOptimistically = useGitStore((state) => state.moveStatusPathsOptimistically);
|
||||
const restoreStatus = useGitStore((state) => state.restoreStatus);
|
||||
const bumpIndexRevision = useGitStore((state) => state.bumpIndexRevision);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const openContextDiff = useUIStore((state) => state.openContextDiff);
|
||||
const navigateToDiff = useUIStore((state) => state.navigateToDiff);
|
||||
const setRightSidebarOpen = useUIStore((state) => state.setRightSidebarOpen);
|
||||
const previousBootstrapStatusRef = React.useRef<'pending' | 'ready' | 'failed' | null>(null);
|
||||
const gitReconcileTimeoutRef = React.useRef<number | null>(null);
|
||||
const gitMutationFlushTimeoutRef = React.useRef<number | null>(null);
|
||||
const flushQueuedGitMutationsRef = React.useRef<(() => void) | null>(null);
|
||||
|
||||
const clearScheduledGitReconcile = React.useCallback(() => {
|
||||
if (gitReconcileTimeoutRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearTimeout(gitReconcileTimeoutRef.current);
|
||||
gitReconcileTimeoutRef.current = null;
|
||||
}, []);
|
||||
|
||||
const scheduleGitReconcile = React.useCallback((directory: string) => {
|
||||
clearScheduledGitReconcile();
|
||||
gitReconcileTimeoutRef.current = window.setTimeout(() => {
|
||||
gitReconcileTimeoutRef.current = null;
|
||||
if (normalizePath(directory) !== normalizePath(currentDirectory)) {
|
||||
return;
|
||||
}
|
||||
void fetchStatus(directory, git, { silent: true });
|
||||
}, GIT_RECONCILE_DELAY_MS);
|
||||
}, [clearScheduledGitReconcile, currentDirectory, fetchStatus, git]);
|
||||
|
||||
React.useEffect(() => clearScheduledGitReconcile, [clearScheduledGitReconcile]);
|
||||
|
||||
const clearScheduledGitMutationFlush = React.useCallback(() => {
|
||||
if (gitMutationFlushTimeoutRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearTimeout(gitMutationFlushTimeoutRef.current);
|
||||
gitMutationFlushTimeoutRef.current = null;
|
||||
}, []);
|
||||
|
||||
const scheduleGitMutationFlush = React.useCallback(() => {
|
||||
if (gitMutationFlushTimeoutRef.current !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
gitMutationFlushTimeoutRef.current = window.setTimeout(() => {
|
||||
gitMutationFlushTimeoutRef.current = null;
|
||||
flushQueuedGitMutationsRef.current?.();
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
const runGitIndexMutation = React.useCallback(async (
|
||||
directory: string,
|
||||
direction: GitIndexMutationDirection,
|
||||
paths: string[]
|
||||
) => {
|
||||
if (direction === 'stage') {
|
||||
if (git.stageGitFiles) {
|
||||
await git.stageGitFiles(directory, paths);
|
||||
return;
|
||||
}
|
||||
await Promise.all(paths.map((filePath) => git.stageGitFile(directory, filePath)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (git.unstageGitFiles) {
|
||||
await git.unstageGitFiles(directory, paths);
|
||||
return;
|
||||
}
|
||||
await Promise.all(paths.map((filePath) => git.unstageGitFile(directory, filePath)));
|
||||
}, [git]);
|
||||
|
||||
const gitIndexMutationQueue = React.useMemo<GitIndexMutationQueue>(() => createGitIndexMutationQueue({
|
||||
runMutation: ({ directory, direction, paths }) => runGitIndexMutation(directory, direction, paths),
|
||||
onMutationComplete: ({ directory }) => {
|
||||
bumpIndexRevision(directory);
|
||||
scheduleGitReconcile(directory);
|
||||
},
|
||||
onMutationError: ({ directory, direction, rollback }, error) => {
|
||||
rollback?.();
|
||||
bumpIndexRevision(directory);
|
||||
scheduleGitReconcile(directory);
|
||||
const fallback = direction === 'stage'
|
||||
? t('gitView.toast.stageFileFailed')
|
||||
: t('gitView.toast.unstageFileFailed');
|
||||
toast.error(error instanceof Error ? error.message : fallback);
|
||||
},
|
||||
onPathsComplete: (paths) => {
|
||||
setMovingChangePaths((previous) => {
|
||||
const updated = new Set(previous);
|
||||
paths.forEach((path) => updated.delete(path));
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
scheduleFlush: scheduleGitMutationFlush,
|
||||
}), [bumpIndexRevision, runGitIndexMutation, scheduleGitMutationFlush, scheduleGitReconcile, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
flushQueuedGitMutationsRef.current = gitIndexMutationQueue.flush;
|
||||
return () => {
|
||||
flushQueuedGitMutationsRef.current = null;
|
||||
};
|
||||
}, [gitIndexMutationQueue]);
|
||||
|
||||
React.useEffect(() => () => gitIndexMutationQueue.clear(), [gitIndexMutationQueue]);
|
||||
|
||||
React.useEffect(() => clearScheduledGitMutationFlush, [clearScheduledGitMutationFlush]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory) {
|
||||
@@ -442,17 +559,15 @@ export const GitView: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [selectedPaths, setSelectedPaths] = React.useState<Set<string>>(
|
||||
() => new Set(initialSnapshot?.selectedPaths ?? [])
|
||||
);
|
||||
const [hasUserAdjustedSelection, setHasUserAdjustedSelection] = React.useState(false);
|
||||
const [revertingPaths, setRevertingPaths] = React.useState<Set<string>>(new Set());
|
||||
const [movingChangePaths, setMovingChangePaths] = React.useState<Set<string>>(new Set());
|
||||
const [isRevertingAll, setIsRevertingAll] = React.useState(false);
|
||||
const [integrateRefreshKey, setIntegrateRefreshKey] = React.useState(0);
|
||||
const [isGeneratingMessage, setIsGeneratingMessage] = React.useState(false);
|
||||
const [generatedHighlights, setGeneratedHighlights] = React.useState<string[]>(
|
||||
initialSnapshot?.generatedHighlights ?? []
|
||||
);
|
||||
const hasPendingIndexMutation = movingChangePaths.size > 0 || gitIndexMutationQueue.size() > 0 || gitIndexMutationQueue.isRunning();
|
||||
|
||||
const scrollActionPanelToBottom = React.useCallback(() => {
|
||||
const scrollTarget = actionPanelScrollRef.current;
|
||||
@@ -662,11 +777,10 @@ export const GitView: React.FC = () => {
|
||||
if (!currentDirectory) return;
|
||||
gitViewSnapshots.set(currentDirectory, {
|
||||
directory: currentDirectory,
|
||||
selectedPaths: Array.from(selectedPaths),
|
||||
commitMessage,
|
||||
generatedHighlights,
|
||||
});
|
||||
}, [commitMessage, currentDirectory, selectedPaths, generatedHighlights]);
|
||||
}, [commitMessage, currentDirectory, generatedHighlights]);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadProfiles();
|
||||
@@ -844,6 +958,16 @@ export const GitView: React.FC = () => {
|
||||
return Array.from(unique.values()).sort((a, b) => a.path.localeCompare(b.path));
|
||||
}, [status]);
|
||||
|
||||
const stagedChangeEntries = React.useMemo(
|
||||
() => changeEntries.filter(isStagedStatusFile),
|
||||
[changeEntries]
|
||||
);
|
||||
|
||||
const unstagedChangeEntries = React.useMemo(
|
||||
() => changeEntries.filter(isUnstagedStatusFile),
|
||||
[changeEntries]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory || changeEntries.length === 0) {
|
||||
return;
|
||||
@@ -860,7 +984,7 @@ export const GitView: React.FC = () => {
|
||||
orderedPaths.push(path);
|
||||
};
|
||||
|
||||
Array.from(selectedPaths).forEach(pushPath);
|
||||
stagedChangeEntries.forEach((entry) => pushPath(entry.path));
|
||||
visibleChangePaths.forEach(pushPath);
|
||||
changeEntries.slice(0, GIT_DIFF_PRIORITY_BASELINE_LIMIT).forEach((entry) => pushPath(entry.path));
|
||||
|
||||
@@ -875,30 +999,7 @@ export const GitView: React.FC = () => {
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [changeEntries, currentDirectory, git, prefetchDiffs, selectedPaths, visibleChangePaths]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!status || changeEntries.length === 0) {
|
||||
setSelectedPaths(new Set());
|
||||
setHasUserAdjustedSelection(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPaths((previous) => {
|
||||
const next = new Set<string>();
|
||||
const previousSet = previous ?? new Set<string>();
|
||||
|
||||
for (const file of changeEntries) {
|
||||
if (previousSet.has(file.path)) {
|
||||
next.add(file.path);
|
||||
} else if (!hasUserAdjustedSelection) {
|
||||
next.add(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}, [status, changeEntries, hasUserAdjustedSelection]);
|
||||
}, [changeEntries, currentDirectory, git, prefetchDiffs, stagedChangeEntries, visibleChangePaths]);
|
||||
|
||||
const handleSyncAction = async (action: Exclude<SyncAction, null>, remote?: GitRemote) => {
|
||||
if (!currentDirectory) return;
|
||||
@@ -1027,9 +1128,9 @@ export const GitView: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToCommit = Array.from(selectedPaths).sort();
|
||||
const filesToCommit = stagedChangeEntries.map((file) => file.path).sort();
|
||||
if (filesToCommit.length === 0) {
|
||||
toast.error(t('gitView.toast.selectFileToCommit'));
|
||||
toast.error(t('gitView.toast.stageFileToCommit'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1039,11 +1140,11 @@ export const GitView: React.FC = () => {
|
||||
try {
|
||||
await git.createGitCommit(currentDirectory, commitMessage.trim(), {
|
||||
files: filesToCommit,
|
||||
stageFiles: [],
|
||||
});
|
||||
bumpIndexRevision(currentDirectory);
|
||||
toast.success(t('gitView.toast.commitCreated'));
|
||||
setCommitMessage('');
|
||||
setSelectedPaths(new Set());
|
||||
setHasUserAdjustedSelection(false);
|
||||
clearGeneratedHighlights();
|
||||
|
||||
await refreshStatusAndBranches();
|
||||
@@ -1120,19 +1221,20 @@ export const GitView: React.FC = () => {
|
||||
|
||||
const handleGenerateCommitMessage = React.useCallback(async () => {
|
||||
if (!currentDirectory) return;
|
||||
if (selectedPaths.size === 0) {
|
||||
toast.error(t('gitView.toast.selectFileToDescribe'));
|
||||
const selectedFilePaths = stagedChangeEntries.map((file) => file.path).sort();
|
||||
if (selectedFilePaths.length === 0) {
|
||||
toast.error(t('gitView.toast.stageFileToDescribe'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[git-generation][browser] generate button clicked', {
|
||||
directory: currentDirectory,
|
||||
selectedFiles: selectedPaths.size,
|
||||
selectedFiles: selectedFilePaths.length,
|
||||
});
|
||||
|
||||
setIsGeneratingMessage(true);
|
||||
try {
|
||||
const { message } = await generateSessionCommitMessage(currentDirectory, Array.from(selectedPaths));
|
||||
const { message } = await generateSessionCommitMessage(currentDirectory, selectedFilePaths);
|
||||
const subject = message.subject?.trim() ?? '';
|
||||
const highlights = Array.isArray(message.highlights) ? message.highlights : [];
|
||||
|
||||
@@ -1163,7 +1265,7 @@ export const GitView: React.FC = () => {
|
||||
} finally {
|
||||
setIsGeneratingMessage(false);
|
||||
}
|
||||
}, [currentDirectory, selectedPaths, settingsGitmojiEnabled, gitmojiEmojis, scrollActionPanelToBottom, t]);
|
||||
}, [currentDirectory, stagedChangeEntries, settingsGitmojiEnabled, gitmojiEmojis, scrollActionPanelToBottom, t]);
|
||||
|
||||
const formatBlockingReason = (reason: ReturnType<typeof getMutationBlockingReasons>[number]): string => {
|
||||
if (reason.reason === 'attention') {
|
||||
@@ -1477,7 +1579,7 @@ export const GitView: React.FC = () => {
|
||||
return globalIdentity ?? null;
|
||||
}, [currentIdentity, profiles, globalIdentity]);
|
||||
|
||||
const selectedCount = selectedPaths.size;
|
||||
const stagedCount = stagedChangeEntries.length;
|
||||
const isBusy = isLoading || syncAction !== null || commitAction !== null;
|
||||
const currentBranch = status?.current ?? null;
|
||||
const canShowIntegrateCommitsSection = Boolean(
|
||||
@@ -1571,29 +1673,25 @@ export const GitView: React.FC = () => {
|
||||
}, [baseBranch, currentBranch, currentDirectory, git, log, logMaxCountLocal]);
|
||||
// Keep these sections stable in layout; individual cards render placeholders when unavailable.
|
||||
|
||||
const toggleFileSelection = (path: string) => {
|
||||
setSelectedPaths((previous) => {
|
||||
const moveChangePaths = React.useCallback((paths: string[], direction: GitIndexMutationDirection) => {
|
||||
if (!currentDirectory || paths.length === 0) return;
|
||||
const uniquePaths = Array.from(new Set(paths));
|
||||
setMovingChangePaths((previous) => {
|
||||
const next = new Set(previous);
|
||||
if (next.has(path)) {
|
||||
next.delete(path);
|
||||
} else {
|
||||
next.add(path);
|
||||
}
|
||||
uniquePaths.forEach((path) => next.add(path));
|
||||
return next;
|
||||
});
|
||||
setHasUserAdjustedSelection(true);
|
||||
};
|
||||
const previousStatus = moveStatusPathsOptimistically(currentDirectory, uniquePaths, direction);
|
||||
|
||||
const selectAll = () => {
|
||||
const next = new Set(changeEntries.map((file) => file.path));
|
||||
setSelectedPaths(next);
|
||||
setHasUserAdjustedSelection(true);
|
||||
};
|
||||
gitIndexMutationQueue.enqueue({
|
||||
directory: currentDirectory,
|
||||
direction,
|
||||
paths: new Set(uniquePaths),
|
||||
rollback: () => restoreStatus(currentDirectory, previousStatus),
|
||||
});
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedPaths(new Set());
|
||||
setHasUserAdjustedSelection(true);
|
||||
};
|
||||
scheduleGitMutationFlush();
|
||||
}, [currentDirectory, gitIndexMutationQueue, moveStatusPathsOptimistically, restoreStatus, scheduleGitMutationFlush]);
|
||||
|
||||
const handleRevertFile = React.useCallback(
|
||||
async (filePath: string) => {
|
||||
@@ -1606,7 +1704,7 @@ export const GitView: React.FC = () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await git.revertGitFile(currentDirectory, filePath);
|
||||
await git.revertGitFile(currentDirectory, filePath, { scope: 'working' });
|
||||
toast.success(t('gitView.toast.revertedFile', { path: filePath }));
|
||||
await refreshStatusAndBranches(false);
|
||||
} catch (err) {
|
||||
@@ -1630,6 +1728,8 @@ export const GitView: React.FC = () => {
|
||||
}
|
||||
|
||||
const uniquePaths = Array.from(new Set(paths));
|
||||
const stagedPaths = new Set(stagedChangeEntries.map((entry) => entry.path));
|
||||
const touchesStagedIndex = uniquePaths.some((path) => stagedPaths.has(path));
|
||||
setIsRevertingAll(true);
|
||||
setRevertingPaths((previous) => {
|
||||
const next = new Set(previous);
|
||||
@@ -1651,6 +1751,10 @@ export const GitView: React.FC = () => {
|
||||
}
|
||||
}));
|
||||
|
||||
if (touchesStagedIndex && failed.length < uniquePaths.length) {
|
||||
bumpIndexRevision(currentDirectory);
|
||||
}
|
||||
|
||||
await refreshStatusAndBranches(false);
|
||||
|
||||
if (failed.length === 0) {
|
||||
@@ -1678,9 +1782,67 @@ export const GitView: React.FC = () => {
|
||||
setIsRevertingAll(false);
|
||||
}
|
||||
},
|
||||
[currentDirectory, git, isRevertingAll, refreshStatusAndBranches, t]
|
||||
[bumpIndexRevision, currentDirectory, git, isRevertingAll, refreshStatusAndBranches, stagedChangeEntries, t]
|
||||
);
|
||||
|
||||
const handleViewChangeDiff = React.useCallback((path: string, staged: boolean) => {
|
||||
if (currentDirectory && !isMobile) {
|
||||
openContextDiff(currentDirectory, path, staged);
|
||||
return;
|
||||
}
|
||||
navigateToDiff(path, staged);
|
||||
if (isMobile) {
|
||||
setRightSidebarOpen(false);
|
||||
}
|
||||
}, [currentDirectory, isMobile, navigateToDiff, openContextDiff, setRightSidebarOpen]);
|
||||
|
||||
const openStashes = React.useCallback(() => setIsStashesDialogOpen(true), []);
|
||||
|
||||
const changeGroups = React.useMemo<ChangesGroupConfig[]>(() => {
|
||||
const groups: ChangesGroupConfig[] = [];
|
||||
|
||||
if (stagedChangeEntries.length > 0) {
|
||||
groups.push({
|
||||
id: 'staged',
|
||||
title: t('gitView.changes.stagedTitle'),
|
||||
entries: stagedChangeEntries,
|
||||
actionSymbol: '-',
|
||||
actionAllLabel: t('gitView.changes.unstageAllAria'),
|
||||
getActionLabel: (path) => t('gitView.changes.unstageFileAria', { path }),
|
||||
onActionFile: (path) => void moveChangePaths([path], 'unstage'),
|
||||
onActionAll: (paths) => void moveChangePaths(paths, 'unstage'),
|
||||
onViewDiff: (path) => handleViewChangeDiff(path, true),
|
||||
onRevertFile: handleRevertFile,
|
||||
showRevertActions: false,
|
||||
accent: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (unstagedChangeEntries.length > 0) {
|
||||
groups.push({
|
||||
id: 'unstaged',
|
||||
title: t('gitView.changes.title'),
|
||||
entries: unstagedChangeEntries,
|
||||
actionSymbol: '+',
|
||||
actionAllLabel: t('gitView.changes.stageAllAria'),
|
||||
getActionLabel: (path) => t('gitView.changes.stageFileAria', { path }),
|
||||
onActionFile: (path) => void moveChangePaths([path], 'stage'),
|
||||
onActionAll: (paths) => void moveChangePaths(paths, 'stage'),
|
||||
onViewDiff: (path) => handleViewChangeDiff(path, false),
|
||||
onRevertFile: handleRevertFile,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
handleRevertFile,
|
||||
handleViewChangeDiff,
|
||||
moveChangePaths,
|
||||
stagedChangeEntries,
|
||||
t,
|
||||
unstagedChangeEntries,
|
||||
]);
|
||||
|
||||
const handleInsertHighlights = React.useCallback((sourceHighlights: string[]) => {
|
||||
if (sourceHighlights.length === 0) return;
|
||||
const normalizedHighlights = sourceHighlights
|
||||
@@ -2007,6 +2169,7 @@ export const GitView: React.FC = () => {
|
||||
const currentBranch = status?.current;
|
||||
const operation = stashDialogOperation;
|
||||
const branch = stashDialogBranch;
|
||||
const hadStagedChanges = (status?.files ?? []).some(isStagedStatusFile);
|
||||
|
||||
// Stash changes
|
||||
try {
|
||||
@@ -2014,6 +2177,9 @@ export const GitView: React.FC = () => {
|
||||
message: `Auto-stash before ${operation} with ${branch}`,
|
||||
includeUntracked: true,
|
||||
});
|
||||
if (hadStagedChanges) {
|
||||
bumpIndexRevision(currentDirectory);
|
||||
}
|
||||
} catch (stashErr) {
|
||||
const msg = stashErr instanceof Error ? stashErr.message : 'Failed to stash changes';
|
||||
toast.error(msg);
|
||||
@@ -2053,6 +2219,7 @@ export const GitView: React.FC = () => {
|
||||
if (restoreAfter && operationSucceeded) {
|
||||
try {
|
||||
await git.stashPop(currentDirectory);
|
||||
bumpIndexRevision(currentDirectory);
|
||||
toast.success(t('gitView.toast.stashedRestored'));
|
||||
} catch (popErr) {
|
||||
const popMessage = popErr instanceof Error ? popErr.message : t('gitView.toast.restoreStashFailed');
|
||||
@@ -2069,6 +2236,7 @@ export const GitView: React.FC = () => {
|
||||
if (restoreAfter) {
|
||||
try {
|
||||
await git.stashPop(currentDirectory);
|
||||
bumpIndexRevision(currentDirectory);
|
||||
} catch {
|
||||
// Ignore stash pop errors in this case
|
||||
}
|
||||
@@ -2076,7 +2244,7 @@ export const GitView: React.FC = () => {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[currentDirectory, git, status, stashDialogOperation, stashDialogBranch, refreshStatusAndBranches, refreshLog, t]
|
||||
[bumpIndexRevision, currentDirectory, git, status, stashDialogOperation, stashDialogBranch, refreshStatusAndBranches, refreshLog, t]
|
||||
);
|
||||
|
||||
if (!currentDirectory) {
|
||||
@@ -2155,6 +2323,7 @@ export const GitView: React.FC = () => {
|
||||
isApplyingIdentity={isSettingIdentity}
|
||||
isWorktreeMode={!!worktreeMetadata}
|
||||
onOpenHistory={() => setIsHistoryDialogOpen(true)}
|
||||
onOpenStashes={openStashes}
|
||||
actionTabItems={actionTabItems}
|
||||
activeActionTab={actionTab}
|
||||
onSelectActionTab={(tabID) => setActionTab(tabID as ActionTab)}
|
||||
@@ -2188,37 +2357,22 @@ export const GitView: React.FC = () => {
|
||||
preventOverscroll
|
||||
>
|
||||
{actionTab === 'commit' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
{(changeEntries?.length ?? 0) > 0 ? (
|
||||
<>
|
||||
<ChangesSection
|
||||
maxListHeightClassName="max-h-[40vh]"
|
||||
changeEntries={changeEntries}
|
||||
onVisiblePathsChange={setVisibleChangePaths}
|
||||
selectedPaths={selectedPaths}
|
||||
diffStats={status?.diffStats}
|
||||
revertingPaths={revertingPaths}
|
||||
onToggleFile={toggleFileSelection}
|
||||
onSelectAll={selectAll}
|
||||
onClearSelection={clearSelection}
|
||||
onRevertAll={handleRevertAll}
|
||||
onViewDiff={(path) => {
|
||||
if (currentDirectory && !isMobile) {
|
||||
openContextDiff(currentDirectory, path);
|
||||
return;
|
||||
}
|
||||
navigateToDiff(path);
|
||||
if (isMobile) {
|
||||
setRightSidebarOpen(false);
|
||||
}
|
||||
}}
|
||||
onRevertFile={handleRevertFile}
|
||||
isRevertingAll={isRevertingAll}
|
||||
onOpenStashes={() => setIsStashesDialogOpen(true)}
|
||||
/>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<ChangesPanel
|
||||
groups={changeGroups}
|
||||
diffStats={status?.diffStats}
|
||||
revertingPaths={revertingPaths}
|
||||
isRevertingAll={isRevertingAll}
|
||||
onVisiblePathsChange={setVisibleChangePaths}
|
||||
onRevertAll={handleRevertAll}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CommitSection
|
||||
selectedCount={selectedCount}
|
||||
stagedCount={stagedCount}
|
||||
commitMessage={commitMessage}
|
||||
onCommitMessageChange={setCommitMessage}
|
||||
generatedHighlights={generatedHighlights}
|
||||
@@ -2228,6 +2382,7 @@ export const GitView: React.FC = () => {
|
||||
onCommit={() => handleCommit({ pushAfter: false })}
|
||||
onCommitAndPush={() => handleCommit({ pushAfter: true })}
|
||||
commitAction={commitAction}
|
||||
hasPendingIndexMutation={hasPendingIndexMutation}
|
||||
gitmojiEnabled={settingsGitmojiEnabled}
|
||||
onOpenGitmojiPicker={() => setIsGitmojiPickerOpen(true)}
|
||||
/>
|
||||
@@ -2339,8 +2494,12 @@ export const GitView: React.FC = () => {
|
||||
onOpenChange={setIsStashesDialogOpen}
|
||||
directory={currentDirectory}
|
||||
hasUncommittedChanges={(status?.files?.length ?? 0) > 0}
|
||||
hasStagedChanges={stagedChangeEntries.length > 0}
|
||||
uncommittedFileCount={status?.files?.length ?? 0}
|
||||
onChanged={async () => {
|
||||
onChanged={async (change) => {
|
||||
if (currentDirectory && change?.affectsIndex) {
|
||||
bumpIndexRevision(currentDirectory);
|
||||
}
|
||||
await refreshStatusAndBranches(false);
|
||||
await refreshLog();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user