fix: snapshot uncommitted entries in combined diff to survive commits (#652)
This commit is contained in:
parent
e342b64e78
commit
033b8bcc31
|
|
@ -14,6 +14,7 @@ import { Button } from '@/components/ui/button'
|
|||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import type { GitBranchChangeEntry, GitDiffResult, GitStatusEntry } from '../../../../shared/types'
|
||||
import { DiffSectionItem } from './DiffSectionItem'
|
||||
import { getCombinedUncommittedEntries } from './combined-diff-entries'
|
||||
|
||||
type DiffSection = {
|
||||
key: string
|
||||
|
|
@ -79,18 +80,24 @@ export default function CombinedDiffViewer({ file }: { file: OpenFile }): React.
|
|||
? file.branchCompare
|
||||
: null
|
||||
|
||||
// Why: prefer the snapshot taken at tab-open time so a commit that changes
|
||||
// gitStatusByWorktree does not rebuild all sections and lose loaded content.
|
||||
// The snapshot is already area-filtered by openAllDiffs; conflict filtering
|
||||
// is applied here via snapshotEntries. The live path (getCombinedUncommittedEntries)
|
||||
// adds its own area + conflict filtering as a fallback for tabs opened before
|
||||
// the snapshot field existed.
|
||||
const snapshotEntries = React.useMemo(
|
||||
() => file.uncommittedEntriesSnapshot?.filter((e) => e.conflictStatus !== 'unresolved'),
|
||||
[file.uncommittedEntriesSnapshot]
|
||||
)
|
||||
const uncommittedEntries = React.useMemo(
|
||||
() =>
|
||||
(gitStatusByWorktree[file.worktreeId] ?? []).filter((entry) => {
|
||||
if (entry.conflictStatus === 'unresolved') {
|
||||
return false
|
||||
}
|
||||
if (file.combinedAreaFilter) {
|
||||
return entry.area === file.combinedAreaFilter
|
||||
}
|
||||
return entry.area !== 'untracked'
|
||||
}),
|
||||
[file.worktreeId, file.combinedAreaFilter, gitStatusByWorktree]
|
||||
snapshotEntries ??
|
||||
getCombinedUncommittedEntries(
|
||||
gitStatusByWorktree[file.worktreeId] ?? [],
|
||||
file.combinedAreaFilter
|
||||
),
|
||||
[snapshotEntries, file.worktreeId, file.combinedAreaFilter, gitStatusByWorktree]
|
||||
)
|
||||
const branchEntries = React.useMemo<GitBranchChangeEntry[]>(() => {
|
||||
const snapshotEntries = file.branchEntriesSnapshot ?? []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getCombinedUncommittedEntries } from './combined-diff-entries'
|
||||
import type { GitStatusEntry } from '../../../../shared/types'
|
||||
|
||||
describe('getCombinedUncommittedEntries', () => {
|
||||
it('filters unresolved conflicts from live entries', () => {
|
||||
const liveEntries: GitStatusEntry[] = [
|
||||
{
|
||||
path: 'src/conflict.ts',
|
||||
status: 'modified',
|
||||
area: 'unstaged',
|
||||
conflictStatus: 'unresolved'
|
||||
},
|
||||
{ path: 'src/ok.ts', status: 'modified', area: 'unstaged' }
|
||||
]
|
||||
|
||||
expect(getCombinedUncommittedEntries(liveEntries, undefined)).toEqual([
|
||||
{ path: 'src/ok.ts', status: 'modified', area: 'unstaged' }
|
||||
])
|
||||
})
|
||||
|
||||
it('applies area filter when provided', () => {
|
||||
const liveEntries: GitStatusEntry[] = [
|
||||
{ path: 'src/staged.ts', status: 'modified', area: 'staged' },
|
||||
{ path: 'src/unstaged.ts', status: 'modified', area: 'unstaged' },
|
||||
{ path: 'src/untracked.ts', status: 'untracked', area: 'untracked' }
|
||||
]
|
||||
|
||||
expect(getCombinedUncommittedEntries(liveEntries, 'staged')).toEqual([
|
||||
{ path: 'src/staged.ts', status: 'modified', area: 'staged' }
|
||||
])
|
||||
})
|
||||
|
||||
it('excludes untracked entries when no area filter is set', () => {
|
||||
const liveEntries: GitStatusEntry[] = [
|
||||
{ path: 'src/staged.ts', status: 'modified', area: 'staged' },
|
||||
{ path: 'src/unstaged.ts', status: 'modified', area: 'unstaged' },
|
||||
{ path: 'src/untracked.ts', status: 'untracked', area: 'untracked' }
|
||||
]
|
||||
|
||||
expect(getCombinedUncommittedEntries(liveEntries, undefined)).toEqual([
|
||||
{ path: 'src/staged.ts', status: 'modified', area: 'staged' },
|
||||
{ path: 'src/unstaged.ts', status: 'modified', area: 'unstaged' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import type { GitStatusEntry } from '../../../../shared/types'
|
||||
|
||||
/**
|
||||
* Fallback filtering for combined-diff tabs that were opened before the
|
||||
* snapshot field existed. When a snapshot is present the caller should use it
|
||||
* directly (after filtering out unresolved conflicts) instead of calling this.
|
||||
*/
|
||||
export function getCombinedUncommittedEntries(
|
||||
liveEntries: GitStatusEntry[],
|
||||
areaFilter: OpenFile['combinedAreaFilter']
|
||||
): GitStatusEntry[] {
|
||||
return liveEntries.filter((entry) => {
|
||||
if (entry.conflictStatus === 'unresolved') {
|
||||
return false
|
||||
}
|
||||
if (areaFilter) {
|
||||
return entry.area === areaFilter
|
||||
}
|
||||
return entry.area !== 'untracked'
|
||||
})
|
||||
}
|
||||
|
|
@ -89,6 +89,10 @@ export type OpenFile = {
|
|||
combinedAlternate?: CombinedDiffAlternate
|
||||
combinedAreaFilter?: string // filter combined diff to a specific area (e.g. 'staged', 'unstaged', 'untracked')
|
||||
branchEntriesSnapshot?: GitBranchChangeEntry[]
|
||||
/** Why: snapshot uncommitted entries at tab-open time so a subsequent commit
|
||||
* does not yank entries out from under the combined diff, which would rebuild
|
||||
* all sections and lose loaded content + scroll position. */
|
||||
uncommittedEntriesSnapshot?: GitStatusEntry[]
|
||||
conflict?: OpenConflictMetadata
|
||||
skippedConflicts?: CombinedDiffSkippedConflict[]
|
||||
conflictReview?: ConflictReviewState
|
||||
|
|
@ -807,6 +811,10 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
const skippedConflicts = relevantEntries
|
||||
.filter((entry) => entry.conflictStatus === 'unresolved' && entry.conflictKind)
|
||||
.map((entry) => ({ path: entry.path, conflictKind: entry.conflictKind! }))
|
||||
// Why: snapshot the entry list at open time so a subsequent commit does
|
||||
// not yank entries from under the combined diff view, which would rebuild
|
||||
// all sections and lose loaded content + scroll position.
|
||||
const uncommittedEntriesSnapshot = relevantEntries
|
||||
const id = areaFilter
|
||||
? `${worktreeId}::all-diffs::uncommitted::${areaFilter}`
|
||||
: `${worktreeId}::all-diffs::uncommitted`
|
||||
|
|
@ -822,6 +830,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
f.id === id
|
||||
? {
|
||||
...f,
|
||||
uncommittedEntriesSnapshot,
|
||||
combinedAlternate: alternate,
|
||||
combinedAreaFilter: areaFilter,
|
||||
skippedConflicts,
|
||||
|
|
@ -845,6 +854,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
isDirty: false,
|
||||
mode: 'diff',
|
||||
diffSource: 'combined-uncommitted',
|
||||
uncommittedEntriesSnapshot,
|
||||
combinedAlternate: alternate,
|
||||
combinedAreaFilter: areaFilter,
|
||||
skippedConflicts,
|
||||
|
|
|
|||
Loading…
Reference in New Issue