fix(source-control): keep status refresh for worktrees whose admin dirs differ only by case (#28586)

On case-sensitive filesystems Git can register two linked worktrees whose
admin dirs are `Foo` and `foo`. The status-attribution memo lookup folds
case, so it kept only one checkout per key and a `git add` in the other
was attributed to its sibling, skipping that worktree's Source Control
refresh. Name every checkout that shares the folded key instead.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-14 05:50:51 +00:00
committed by GitHub
co-authored by Claude
parent 5e1c117a86
commit 5eda0a0dbd
2 changed files with 32 additions and 5 deletions
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { cachedWorktreePathsForScope } from './worktree-head-identity-cached-paths'
import { createWorktreeHeadIdentityCache } from './worktree-head-identity-reader'
import { headIdentityScopeForEntry } from './worktree-head-identity-scope'
const HEAD = 'a'.repeat(40)
describe('cachedWorktreePathsForScope', () => {
it('names every checkout whose admin dir folds to the same key', () => {
// Case-sensitive filesystems let Git create both `Foo` and `foo` admin dirs.
const cache = createWorktreeHeadIdentityCache()
cache.entries.set('Foo', { worktreePath: '/a/Foo', head: HEAD, branch: 'refs/heads/x' })
cache.entries.set('foo', { worktreePath: '/b/foo', head: HEAD, branch: 'refs/heads/y' })
const paths = cachedWorktreePathsForScope(cache, headIdentityScopeForEntry('Foo'))
expect(paths).toEqual(expect.arrayContaining(['/a/Foo', '/b/foo']))
})
it('stays unattributed when an entry is not in the memo', () => {
const cache = createWorktreeHeadIdentityCache()
expect(cachedWorktreePathsForScope(cache, headIdentityScopeForEntry('wt'))).toBeNull()
})
})
@@ -20,16 +20,19 @@ export function cachedWorktreePathsForScope(
}
paths.push(cache.primary.worktreePath)
}
const pathsByEntryKey = new Map<string, string>()
// Why: keys fold case, so on case-sensitive filesystems `Foo` and `foo` are
// distinct admin dirs sharing one key; name both rather than drop the writer.
const pathsByEntryKey = new Map<string, string[]>()
for (const [name, identity] of cache.entries) {
pathsByEntryKey.set(headIdentityEntryKey(name), identity.worktreePath)
const key = headIdentityEntryKey(name)
pathsByEntryKey.set(key, [...(pathsByEntryKey.get(key) ?? []), identity.worktreePath])
}
for (const key of scope.entryNames) {
const path = pathsByEntryKey.get(key)
if (path === undefined) {
const entryPaths = pathsByEntryKey.get(key)
if (entryPaths === undefined) {
return null
}
paths.push(path)
paths.push(...entryPaths)
}
return paths
}