diff --git a/features/gitLog.feature b/features/gitLog.feature index dc1f6eb5..880e063f 100644 --- a/features/gitLog.feature +++ b/features/gitLog.feature @@ -65,36 +65,42 @@ Feature: Git Log Window # Wait for git log data to stabilize - increased from implicit to explicit And I wait for 2 seconds for "git log data to load" Then I should see a "uncommitted changes row" element with selector "[data-testid='uncommitted-changes-row']" - # Click on the uncommitted changes row - When I click on a "uncommitted changes row" element with selector "[data-testid='uncommitted-changes-row']" - # Verify we can see the modified Index.tid file + # Verify uncommitted changes is auto-selected by checking if the file list is visible Then I should see a "Index.tid file in uncommitted list" element with selector "li:has-text('Index.tid')" - # Switch to Actions tab + # Switch to Actions tab to access commit button When I click on a "actions tab" element with selector "button[role='tab']:has-text('操作')" # Verify the commit now button is visible Then I should see a "commit now button" element with selector "button[data-testid='commit-now-button']" # Click the commit now button When I click on a "commit now button" element with selector "button[data-testid='commit-now-button']" + # Wait for git commit to complete first Then I wait for "git commit completed" log marker "[test-id-git-commit-complete]" - # Wait for git log data to be updated and rendered to DOM - Then I wait for "git log data rendered to DOM" log marker "[test-id-git-log-data-rendered]" - # After commit, verify the new commit with default message in p tag - And I should see a "commit with default message" element with selector "p.MuiTypography-body2:has-text('使用太记桌面版备份')" - # Don't need to Click on the commit row we just created (contains the commit message) Because we should automatically select it - And I wait for 1 seconds for "commit details panel to load and git lock to release" - # Don't need to Switch to Actions tab to test rollback, because we are already on Actions tab - # Wait for revert button to be visible (should auto-select first commit after commit) - Then I should see a "revert button" element with selector "button:has-text('回滚')" + # Clear old git-log-data-rendered markers to wait for the new one after commit + When I clear log lines containing "[test-id-git-log-data-rendered]" + # Then wait for UI to render the updated data (without uncommitted changes) + Then I wait for "git log data rendered after commit" log marker "[test-id-git-log-data-rendered]" + # Verify that uncommitted changes row is gone (commit was successful) + Then I should not see a "uncommitted changes row" element with selector "[data-testid='uncommitted-changes-row']" + # Verify the correct commit is selected and we're on the latest commit (should show amend button) + Then I should see "selected commit row and commit message and amend button and revert button" elements with selectors: + | [data-testid^='commit-row-'][data-selected='true']:has-text('使用太记桌面版备份') | + | p.MuiTypography-body2:has-text('使用太记桌面版备份') | + | button:has-text('修改') | + | button:has-text('回滚') | # Click revert button When I click on a "revert button" element with selector "button:has-text('回滚')" - # Wait for git revert operation to complete - git operations can be slow on CI and may take longer than usual when system is under load - # The git revert process involves file system operations that may be queued by the OS + # Wait for git revert operation to complete and UI to render the new revert commit Then I wait for "git revert completed" log marker "[test-id-git-revert-complete]" + When I clear log lines containing "[test-id-git-log-data-rendered]" + Then I wait for "git log data rendered after revert" log marker "[test-id-git-log-data-rendered]" + # Verify that the new revert commit is auto-selected (should contain "回退提交" in the message) + Then I should see a "selected revert commit row" element with selector "[data-testid^='commit-row-'][data-selected='true']:has-text('回退提交')" + # Also verify the revert button is visible (confirms we're on the new commit) + Then I should see a "revert button for the new revert commit" element with selector "button:has-text('回滚')" # Switch back to main window to verify the revert When I switch to "main" window - # Wait for file system events to stabilize after git revert - the delete-then-recreate events need time to propagate through nsfw watcher - # The watch-fs plugin uses a 100ms delay to handle git operations that delete-then-recreate files - And I wait for 2 seconds for "file system events to stabilize after git revert" + # Wait for tiddler to be updated by watch-fs after git revert + Then I wait for tiddler "Index" to be updated by watch-fs # The modified content should be reverted, and make sure file won't be deleted Then I should not see a "missing tiddler indicator" element in browser view with selector "[data-tiddler-title='Index']:has-text('佚失')" Then I should not see a "modified content in Index tiddler" element in browser view with selector "[data-tiddler-title='Index']:has-text('Modified Index content')" diff --git a/src/services/wiki/plugin/watchFileSystemAdaptor/FileSystemWatcher.ts b/src/services/wiki/plugin/watchFileSystemAdaptor/FileSystemWatcher.ts index 8a7f0a39..96d33bd3 100644 --- a/src/services/wiki/plugin/watchFileSystemAdaptor/FileSystemWatcher.ts +++ b/src/services/wiki/plugin/watchFileSystemAdaptor/FileSystemWatcher.ts @@ -156,20 +156,6 @@ export class FileSystemWatcher { return; } - // Re-fetch workspace config to get the latest enableFileSystemWatch value - // This ensures we pick up config changes that happened after constructor - if (this.workspaceID) { - try { - const latestConfig = await workspace.get(this.workspaceID); - if (latestConfig && typeof latestConfig === 'object') { - this.workspaceConfig = latestConfig as IWikiWorkspace; - this.logger.log(`FileSystemWatcher Re-fetched workspace config, enableFileSystemWatch=${this.workspaceConfig.enableFileSystemWatch}`); - } - } catch (error) { - this.logger.log(`FileSystemWatcher Failed to re-fetch workspace config: ${error instanceof Error ? error.message : String(error)}`); - } - } - // Check if file system watch is enabled for this workspace if (this.workspaceConfig && !this.workspaceConfig.enableFileSystemWatch) { this.logger.log('[test-id-WATCH_FS_STABILIZED] Watcher has stabilized (disabled by config)'); diff --git a/src/windows/GitLog/CommitTableRow.tsx b/src/windows/GitLog/CommitTableRow.tsx index 24c8831b..596d6da2 100644 --- a/src/windows/GitLog/CommitTableRow.tsx +++ b/src/windows/GitLog/CommitTableRow.tsx @@ -31,6 +31,7 @@ export function CommitTableRow({ commit, selected, commitDate, onSelect, onSyncC selected={selected} onClick={onSelect} data-testid={commit.hash === '' ? 'uncommitted-changes-row' : `commit-row-${commit.hash}`} + data-selected={selected} > [0], 't'>) => CustomGitTooltip({ ...props, t }); - const { handleCommitSuccess, handleUndoSuccess, handleSearch } = useCommitSelection({ + const { handleCommitSuccess, handleRevertSuccess, handleUndoSuccess, handleSearch } = useCommitSelection({ shouldSelectFirst, entries, setShouldSelectFirst, @@ -208,7 +208,7 @@ export default function GitHistory(): React.JSX.Element { onFileSelect={setSelectedFile} selectedFile={selectedFile} onCommitSuccess={handleCommitSuccess} - onRevertSuccess={handleCommitSuccess} + onRevertSuccess={handleRevertSuccess} onUndoSuccess={handleUndoSuccess} isLatestCommit={selectedCommit?.hash === entries.find((entry) => entry.hash !== '')?.hash} /> diff --git a/src/windows/GitLog/useGitHistoryLogic.ts b/src/windows/GitLog/useGitHistoryLogic.ts index c1b52b2a..96a45eab 100644 --- a/src/windows/GitLog/useGitHistoryLogic.ts +++ b/src/windows/GitLog/useGitHistoryLogic.ts @@ -1,4 +1,4 @@ -import { type Dispatch, type SetStateAction, useCallback, useEffect, useState } from 'react'; +import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { GitLogEntry, ISearchParameters } from './types'; @@ -91,6 +91,7 @@ export function useSyncHandler({ workspaceInfo, isSyncing, showSnackbar }: IUseS interface IUseCommitSelectionReturn { handleCommitSuccess: () => void; + handleRevertSuccess: () => void; handleUndoSuccess: () => void; handleSearch: (parameters: ISearchParameters) => void; } @@ -120,6 +121,30 @@ export function useCommitSelection({ setCurrentSearchParameters, setSelectedFile, }: IUseCommitSelectionProps): IUseCommitSelectionReturn { + // Track if we've already processed the current change type + const lastProcessedChangeRef = useRef(null); + // Track if we've done initial selection + const hasInitialSelectionRef = useRef(false); + + // Auto-select on initial load: uncommitted changes if present, otherwise first commit + useEffect(() => { + if (!hasInitialSelectionRef.current && entries.length > 0 && !selectedCommit) { + // First try to find uncommitted changes + const uncommittedEntry = entries.find((entry) => entry.hash === ''); + if (uncommittedEntry) { + setSelectedCommit(uncommittedEntry); + hasInitialSelectionRef.current = true; + } else { + // If no uncommitted changes, select the first commit + const firstCommit = entries[0]; + if (firstCommit) { + setSelectedCommit(firstCommit); + hasInitialSelectionRef.current = true; + } + } + } + }, [entries, selectedCommit, setSelectedCommit]); + // Auto-select first commit after successful manual commit useEffect(() => { if (shouldSelectFirst && entries.length > 0) { @@ -132,49 +157,69 @@ export function useCommitSelection({ } }, [shouldSelectFirst, entries, setSelectedCommit, setShouldSelectFirst]); - // Auto-select first commit when a new commit is detected - useEffect(() => { - if (lastChangeType === 'commit' && entries.length > 0) { - // Find the first non-uncommitted commit (the newly created commit) - const firstCommit = entries.find((entry) => entry.hash !== ''); - if (firstCommit) { - setSelectedCommit(firstCommit); - } - } - }, [lastChangeType, entries, setSelectedCommit]); - - // Auto-select uncommitted changes after undo - useEffect(() => { - if (lastChangeType === 'undo' && entries.length > 0) { - // Find uncommitted entry (hash === '') - const uncommittedEntry = entries.find((entry) => entry.hash === ''); - if (uncommittedEntry) { - setSelectedCommit(uncommittedEntry); - } else { - // If no uncommitted changes, deselect - setSelectedCommit(undefined); - } - } - }, [lastChangeType, entries, setSelectedCommit]); - // Maintain selection across refreshes by hash - // Skip if we should select first (manual commit) or if a commit just happened (auto-selection in progress) useEffect(() => { - if (selectedCommit && entries.length > 0 && !shouldSelectFirst && lastChangeType !== 'commit') { + if (selectedCommit && entries.length > 0 && !shouldSelectFirst) { // Try to find the same commit in the new entries const stillExists = entries.find((entry) => entry.hash === selectedCommit.hash); - // Only update if data actually changed (compare by serialization) - if (stillExists && JSON.stringify(stillExists) !== JSON.stringify(selectedCommit)) { - // Update to the new entry object to get fresh data - setSelectedCommit(stillExists); + + if (stillExists) { + // Only update if data actually changed (compare by serialization) + if (JSON.stringify(stillExists) !== JSON.stringify(selectedCommit)) { + // Update to the new entry object to get fresh data + void window.service.native.log('debug', '[test-id-selection-maintained]', { hash: stillExists.hash, message: stillExists.message }); + setSelectedCommit(stillExists); + } + } else if (selectedCommit.hash === '') { + // If selected uncommitted changes no longer exist (e.g., after commit) + // Select the first non-uncommitted commit + const firstCommit = entries.find((entry) => entry.hash !== ''); + if (firstCommit) { + void window.service.native.log('debug', '[test-id-selection-switched-from-uncommitted]', { oldHash: selectedCommit.hash, newHash: firstCommit.hash, newMessage: firstCommit.message }); + setSelectedCommit(firstCommit); + } } } - }, [entries, selectedCommit, shouldSelectFirst, lastChangeType, setSelectedCommit]); + }, [entries, selectedCommit, shouldSelectFirst, setSelectedCommit]); + + // Handle post-operation selection based on lastChangeType + useEffect(() => { + // Skip if we've already processed this change type + if (lastChangeType && lastChangeType !== lastProcessedChangeRef.current) { + if (lastChangeType === 'revert' && entries.length > 0) { + // After revert, wait for the new revert commit to appear in entries + // The new revert commit should be the first one and different from the currently selected one + const firstCommit = entries.find((entry) => entry.hash !== ''); + // Only auto-select if the first commit is different from what's currently selected + // This ensures we're selecting the NEW revert commit, not staying on the old one + if (firstCommit && (!selectedCommit || firstCommit.hash !== selectedCommit.hash)) { + void window.service.native.log('debug', '[test-id-revert-auto-select]', { hash: firstCommit.hash, message: firstCommit.message }); + setSelectedCommit(firstCommit); + lastProcessedChangeRef.current = lastChangeType; + } + } else if (lastChangeType === 'undo' && entries.length > 0) { + // After undo, select uncommitted changes if they exist + const uncommittedEntry = entries.find((entry) => entry.hash === ''); + if (uncommittedEntry) { + void window.service.native.log('debug', '[test-id-undo-auto-select]', { message: 'Selected uncommitted changes' }); + setSelectedCommit(uncommittedEntry); + lastProcessedChangeRef.current = lastChangeType; + } + } + } + }, [lastChangeType, entries, setSelectedCommit, selectedCommit]); const handleCommitSuccess = useCallback(() => { - // Trigger selection of first commit after data refreshes - setShouldSelectFirst(true); - }, [setShouldSelectFirst]); + // Don't set shouldSelectFirst - let the maintain selection logic handle it + // The uncommitted changes will disappear and it will auto-select the new commit + void window.service.native.log('debug', '[test-id-commit-success-handler]', {}); + }, []); + + const handleRevertSuccess = useCallback(() => { + // After revert, select the new revert commit (first non-uncommitted) + void window.service.native.log('debug', '[test-id-revert-success-handler]', {}); + setLastChangeType('revert'); + }, [setLastChangeType]); const handleUndoSuccess = useCallback(() => { // After undo, we want to select uncommitted changes @@ -194,7 +239,7 @@ export function useCommitSelection({ [setSearchParams, setCurrentSearchParameters, setSelectedCommit, setSelectedFile], ); - return { handleCommitSuccess, handleUndoSuccess, handleSearch }; + return { handleCommitSuccess, handleRevertSuccess, handleUndoSuccess, handleSearch }; } interface IUseInfiniteScrollReturn { diff --git a/src/windows/GitLog/useGitLogData.ts b/src/windows/GitLog/useGitLogData.ts index b6c811f4..e42900d5 100644 --- a/src/windows/GitLog/useGitLogData.ts +++ b/src/windows/GitLog/useGitLogData.ts @@ -33,7 +33,6 @@ export function useGitLogData(workspaceID: string): IGitLogData { const [currentPage, setCurrentPage] = useState(0); const [totalCount, setTotalCount] = useState(0); const [searchParameters, setSearchParameters] = useState({ mode: 'none', query: '', startDate: null, endDate: null }); - const lastLoggedEntriesCount = useRef(0); const lastRefreshTime = useRef(0); const lastChangeTimestamp = useRef(0); const loadingMoreReference = useRef(false); @@ -99,19 +98,21 @@ export function useGitLogData(workspaceID: string): IGitLogData { return; } + // User-initiated operations should always trigger refresh immediately + const userOperations = ['commit', 'sync', 'revert', 'undo', 'checkout', 'discard']; + const isUserOperation = change?.type && userOperations.includes(change.type); + // For file-change events, use longer debounce (1000ms) to avoid watch-fs storm // For other git operations (commit, discard, etc), use shorter debounce (300ms) + // User operations bypass debounce entirely const debounceTime = change?.type === 'file-change' ? 1000 : 300; - // Allow refresh if enough time has passed since last refresh - if (timeSinceLastRefresh >= debounceTime) { + // Allow refresh if enough time has passed since last refresh OR if it's a user operation + if (isUserOperation || timeSinceLastRefresh >= debounceTime) { lastRefreshTime.current = now; lastChangeTimestamp.current = change?.timestamp ?? 0; // Store the type of change so we can auto-select first commit after a manual commit - // Don't let file-change events override commit/undo events to preserve auto-selection behavior - if (change?.type !== 'file-change' || !lastChangeType || lastChangeType === 'file-change') { - setLastChangeType(change?.type ?? null); - } + setLastChangeType(change?.type ?? null); // Trigger refresh when git state changes setRefreshTrigger((previous) => previous + 1); } @@ -220,17 +221,24 @@ export function useGitLogData(workspaceID: string): IGitLogData { void loadGitLog(); }, [workspaceInfo, refreshTrigger, searchParameters]); - // Log when entries are updated and rendered to DOM + // Track the last logged entries to detect actual changes + const lastLoggedEntriesRef = useRef(''); + + // Log when entries are actually updated and rendered to DOM useEffect(() => { if (entries.length > 0 && workspaceInfo && 'wikiFolderLocation' in workspaceInfo) { - // Only log if the entries count actually changed (to avoid logging on every re-render) - if (lastLoggedEntriesCount.current !== entries.length) { - lastLoggedEntriesCount.current = entries.length; + // Create a fingerprint of current entries to detect real changes + const entriesFingerprint = entries.map(e => e.hash || 'uncommitted').join(','); + + // Only log if entries actually changed + if (entriesFingerprint !== lastLoggedEntriesRef.current) { + lastLoggedEntriesRef.current = entriesFingerprint; // Use setTimeout to ensure DOM has been updated after state changes setTimeout(() => { void window.service.native.log('debug', '[test-id-git-log-data-rendered]', { commitCount: entries.length, wikiFolderLocation: workspaceInfo.wikiFolderLocation, + entriesFingerprint, }); }, 100); }