From 89490ae373bbfa0990e1beab4c6d74ed728b2665 Mon Sep 17 00:00:00 2001 From: atakan g Date: Mon, 20 Jul 2026 00:12:00 +0300 Subject: [PATCH] fix(desktop): honor gitignore across Windows path casing --- .../src/app/right-sidebar/files/ipc.test.ts | 27 +++++++++++++++++++ .../src/app/right-sidebar/files/ipc.ts | 13 +++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/right-sidebar/files/ipc.test.ts b/apps/desktop/src/app/right-sidebar/files/ipc.test.ts index bcaddad55b5b..f4cc61b393ad 100644 --- a/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +++ b/apps/desktop/src/app/right-sidebar/files/ipc.test.ts @@ -82,6 +82,33 @@ describe('readProjectDir', () => { expect(readFileDataUrl).toHaveBeenCalledWith('C:/repo/.gitignore') }) + it('filters gitignored entries when Windows path casing differs across IPC results', async () => { + gitRoot.mockResolvedValue('C:\\Repo') + readDir.mockImplementation(async path => { + if (path === 'c:\\repo\\src') { + return ok([ + { name: 'debug.log', path: 'c:\\repo\\src\\debug.log', isDirectory: false }, + { name: 'keep.ts', path: 'c:\\repo\\src\\keep.ts', isDirectory: false } + ]) + } + + if (path === 'C:/Repo') { + return ok([{ name: '.gitignore', path: 'C:/Repo/.gitignore', isDirectory: false }]) + } + + if (path === 'C:/Repo/src') { + return ok([]) + } + + return ok([]) + }) + readFileDataUrl.mockResolvedValue(dataUrl('src/*.log\n')) + + const result = await readProjectDir('c:\\repo\\src', 'c:\\repo') + + expect(result.entries.map(entry => entry.name)).toEqual(['keep.ts']) + }) + it('does not fetch .gitignore contents when listings do not contain .gitignore', async () => { gitRoot.mockResolvedValue('/repo') readDir.mockImplementation(async path => { diff --git a/apps/desktop/src/app/right-sidebar/files/ipc.ts b/apps/desktop/src/app/right-sidebar/files/ipc.ts index 7f60e4a20a1a..29fc46859255 100644 --- a/apps/desktop/src/app/right-sidebar/files/ipc.ts +++ b/apps/desktop/src/app/right-sidebar/files/ipc.ts @@ -32,16 +32,25 @@ function clean(path: string) { return path.replace(/\\/g, '/').replace(/\/+$/, '') || '/' } +// Windows path identity is case-insensitive. Fold only comparison keys so the +// relative path returned below keeps the filesystem's original spelling; +// POSIX paths remain case-sensitive. +function comparisonPath(path: string) { + return /^[A-Za-z]:(?:\/|$)/.test(path) || path.startsWith('//') ? path.toLowerCase() : path +} + /** Strict POSIX-style relative path; null if `child` is not inside `root`. */ function relativeTo(root: string, child: string) { const r = clean(root) const c = clean(child) + const rKey = comparisonPath(r) + const cKey = comparisonPath(c) - if (c === r) { + if (cKey === rKey) { return '' } - return c.startsWith(`${r}/`) ? c.slice(r.length + 1) : null + return cKey.startsWith(`${rKey}/`) ? c.slice(r.length + 1) : null } /** Repo-root → repo-root/a → repo-root/a/b → … for every dir between root and `dir`. */