From 3ad4d286e52be851e91a6eeb1ec0dd7ad86a2c1f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 18 May 2026 22:15:21 +0000 Subject: [PATCH 1/3] Fix git extension not working when repo root is "/" (chroot) The normalizePath() function in the git extension's util.ts would strip the trailing "/" from the root path to get an empty string, which path.normalize() then converts to ".". This broke isDescendant() and pathEquals() for any repository with root path "/", which is common in chroot environments. The fix adds a length > 1 check before stripping trailing separators, preserving root paths like "/" on Linux/macOS (and "C:\" on Windows where normalize restores the separator anyway). Fixes microsoft/vscode#298003 Co-authored-by: Franek Korta --- extensions/git/src/test/git.test.ts | 47 ++++++++++++++++++++++++++++- extensions/git/src/util.ts | 5 +-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index b9c08fb907fa0..8fb72bfdb0ae4 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -6,7 +6,7 @@ import 'mocha'; import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles, parseGitRemotes, parseCoAuthors } from '../git'; import * as assert from 'assert'; -import { splitInChunks } from '../util'; +import { splitInChunks, isDescendant, pathEquals, relativePath } from '../util'; suite('git', () => { suite('GitStatusParser', () => { @@ -644,6 +644,51 @@ suite('git', () => { }); }); + suite('isDescendant', () => { + test('regular paths', function () { + if (process.platform !== 'win32') { + assert.strictEqual(isDescendant('/foo', '/foo/bar'), true); + assert.strictEqual(isDescendant('/foo/', '/foo/bar'), true); + assert.strictEqual(isDescendant('/foo', '/bar/baz'), false); + assert.strictEqual(isDescendant('/foo', '/foo'), true); + } + }); + + test('root path "/" (chroot environments)', function () { + if (process.platform !== 'win32') { + assert.strictEqual(isDescendant('/', '/foo'), true); + assert.strictEqual(isDescendant('/', '/foo/bar'), true); + assert.strictEqual(isDescendant('/', '/'), true); + } + }); + }); + + suite('pathEquals', () => { + test('regular paths', function () { + if (process.platform !== 'win32') { + assert.strictEqual(pathEquals('/foo', '/foo'), true); + assert.strictEqual(pathEquals('/foo/', '/foo'), true); + assert.strictEqual(pathEquals('/foo', '/bar'), false); + } + }); + + test('root path "/"', function () { + if (process.platform !== 'win32') { + assert.strictEqual(pathEquals('/', '/'), true); + assert.strictEqual(pathEquals('/', '/foo'), false); + } + }); + }); + + suite('relativePath', () => { + test('root path "/" as base (chroot environments)', function () { + if (process.platform !== 'win32') { + assert.strictEqual(relativePath('/', '/foo'), 'foo'); + assert.strictEqual(relativePath('/', '/foo/bar'), 'foo/bar'); + } + }); + }); + suite('splitInChunks', () => { test('unit tests', function () { assert.deepStrictEqual( diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index dfe3c4e07757d..cb0a2e2f41bfe 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -313,8 +313,9 @@ function normalizePath(path: string): string { } // Trailing separator - if (/[/\\]$/.test(path)) { - // Remove trailing separator + if (path.length > 1 && /[/\\]$/.test(path)) { + // Remove trailing separator, but preserve root paths + // (e.g., "/" on Linux/macOS or "C:\" on Windows) path = path.substring(0, path.length - 1); } From 9bfe79a227f213caa12160962ea6a1d8f78a9e93 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 21:15:23 +0000 Subject: [PATCH 2/3] Use root-aware check instead of length guard Use dirname() to detect filesystem roots instead of a length > 1 check. This correctly preserves all root paths: "/" on POSIX, "C:\" and UNC roots on Windows, rather than relying on normalize() to restore them. Co-authored-by: Franek Korta --- extensions/git/src/util.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index cb0a2e2f41bfe..0b8c2eb4d8d77 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -312,10 +312,9 @@ function normalizePath(path: string): string { path = path.toLowerCase(); } - // Trailing separator - if (path.length > 1 && /[/\\]$/.test(path)) { - // Remove trailing separator, but preserve root paths - // (e.g., "/" on Linux/macOS or "C:\" on Windows) + // Trailing separator — only strip when the path is not + // a filesystem root (e.g., "/" on POSIX, "C:\" or UNC roots on Windows). + if (/[/\\]$/.test(path) && path !== dirname(path)) { path = path.substring(0, path.length - 1); } From 366ba64373ffa732a626528acd758395b6367e95 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 21:17:41 +0000 Subject: [PATCH 3/3] Add Windows drive-root test cases for path utilities Add win32-guarded tests for isDescendant, pathEquals, and relativePath covering C:\ drive roots, ensuring the dirname()-based root detection does not regress on Windows. Co-authored-by: Franek Korta --- extensions/git/src/test/git.test.ts | 43 +++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 8fb72bfdb0ae4..51042a9a845aa 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -645,7 +645,7 @@ suite('git', () => { }); suite('isDescendant', () => { - test('regular paths', function () { + test('regular paths (posix)', function () { if (process.platform !== 'win32') { assert.strictEqual(isDescendant('/foo', '/foo/bar'), true); assert.strictEqual(isDescendant('/foo/', '/foo/bar'), true); @@ -661,10 +661,27 @@ suite('git', () => { assert.strictEqual(isDescendant('/', '/'), true); } }); + + test('regular paths (win32)', function () { + if (process.platform === 'win32') { + assert.strictEqual(isDescendant('C:\\foo', 'C:\\foo\\bar'), true); + assert.strictEqual(isDescendant('C:\\foo\\', 'C:\\foo\\bar'), true); + assert.strictEqual(isDescendant('C:\\foo', 'C:\\bar\\baz'), false); + assert.strictEqual(isDescendant('C:\\foo', 'C:\\foo'), true); + } + }); + + test('drive root "C:\\" (win32)', function () { + if (process.platform === 'win32') { + assert.strictEqual(isDescendant('C:\\', 'C:\\foo'), true); + assert.strictEqual(isDescendant('C:\\', 'C:\\foo\\bar'), true); + assert.strictEqual(isDescendant('C:\\', 'C:\\'), true); + } + }); }); suite('pathEquals', () => { - test('regular paths', function () { + test('regular paths (posix)', function () { if (process.platform !== 'win32') { assert.strictEqual(pathEquals('/foo', '/foo'), true); assert.strictEqual(pathEquals('/foo/', '/foo'), true); @@ -678,6 +695,21 @@ suite('git', () => { assert.strictEqual(pathEquals('/', '/foo'), false); } }); + + test('regular paths (win32)', function () { + if (process.platform === 'win32') { + assert.strictEqual(pathEquals('C:\\foo', 'C:\\foo'), true); + assert.strictEqual(pathEquals('C:\\foo\\', 'C:\\foo'), true); + assert.strictEqual(pathEquals('C:\\foo', 'C:\\bar'), false); + } + }); + + test('drive root "C:\\" (win32)', function () { + if (process.platform === 'win32') { + assert.strictEqual(pathEquals('C:\\', 'C:\\'), true); + assert.strictEqual(pathEquals('C:\\', 'C:\\foo'), false); + } + }); }); suite('relativePath', () => { @@ -687,6 +719,13 @@ suite('git', () => { assert.strictEqual(relativePath('/', '/foo/bar'), 'foo/bar'); } }); + + test('drive root "C:\\" as base (win32)', function () { + if (process.platform === 'win32') { + assert.strictEqual(relativePath('C:\\', 'C:\\foo'), 'foo'); + assert.strictEqual(relativePath('C:\\', 'C:\\foo\\bar'), 'foo\\bar'); + } + }); }); suite('splitInChunks', () => {