Skip to content

Commit 0c768d0

Browse files
committed
fix(worktree): handle Windows native realpath prefixes in path comparison
1 parent b02bbac commit 0c768d0

5 files changed

Lines changed: 33 additions & 10 deletions

File tree

dist/bin/cc-safety-net.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,10 +1740,14 @@ function sameFilesystemPath(left, right) {
17401740
return true;
17411741
}
17421742
} catch {}
1743-
return normalizePathForComparison(realpathSync(left)) === normalizePathForComparison(realpathSync(right));
1743+
return getCanonicalPathForComparison(left) === getCanonicalPathForComparison(right);
1744+
}
1745+
function getCanonicalPathForComparison(path) {
1746+
return normalizePathForComparison(realpathSync.native(path));
17441747
}
17451748
function normalizePathForComparison(path) {
1746-
let normalized = path.replace(/\\/g, "/");
1749+
let normalized = path.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/i, "");
1750+
normalized = normalized.replace(/\\/g, "/");
17471751
if (normalized.length > 1 && normalized.endsWith("/")) {
17481752
normalized = normalized.slice(0, -1);
17491753
}

dist/core/worktree.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export interface GitExecutionContext {
77
export declare function hasGitContextEnvOverride(envAssignments?: ReadonlyMap<string, string>): boolean;
88
export declare function getGitExecutionContext(tokens: readonly string[], cwd: string | undefined): GitExecutionContext;
99
export declare function isLinkedWorktree(cwd: string): boolean;
10+
/** @internal Exported for testing */
11+
export declare function normalizePathForComparison(path: string): string;

dist/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,10 +594,14 @@ function sameFilesystemPath(left, right) {
594594
return true;
595595
}
596596
} catch {}
597-
return normalizePathForComparison(realpathSync(left)) === normalizePathForComparison(realpathSync(right));
597+
return getCanonicalPathForComparison(left) === getCanonicalPathForComparison(right);
598+
}
599+
function getCanonicalPathForComparison(path) {
600+
return normalizePathForComparison(realpathSync.native(path));
598601
}
599602
function normalizePathForComparison(path) {
600-
let normalized = path.replace(/\\/g, "/");
603+
let normalized = path.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/i, "");
604+
normalized = normalized.replace(/\\/g, "/");
601605
if (normalized.length > 1 && normalized.endsWith("/")) {
602606
normalized = normalized.slice(0, -1);
603607
}

src/core/worktree.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,17 @@ function sameFilesystemPath(left: string, right: string): boolean {
209209
// Fall through to realpath comparison for platforms where stat identity is unavailable.
210210
}
211211

212-
return (
213-
normalizePathForComparison(realpathSync(left)) ===
214-
normalizePathForComparison(realpathSync(right))
215-
);
212+
return getCanonicalPathForComparison(left) === getCanonicalPathForComparison(right);
216213
}
217214

218-
function normalizePathForComparison(path: string): string {
219-
let normalized = path.replace(/\\/g, '/');
215+
function getCanonicalPathForComparison(path: string): string {
216+
return normalizePathForComparison(realpathSync.native(path));
217+
}
218+
219+
/** @internal Exported for testing */
220+
export function normalizePathForComparison(path: string): string {
221+
let normalized = path.replace(/^\\\\\?\\UNC\\/i, '//').replace(/^\\\\\?\\/i, '');
222+
normalized = normalized.replace(/\\/g, '/');
220223
if (normalized.length > 1 && normalized.endsWith('/')) {
221224
normalized = normalized.slice(0, -1);
222225
}

tests/core/worktree.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getGitExecutionContext,
1515
hasGitContextEnvOverride,
1616
isLinkedWorktree,
17+
normalizePathForComparison,
1718
} from '@/core/worktree';
1819
import {
1920
createLinkedWorktreeFixture,
@@ -180,6 +181,15 @@ describe('worktree env context overrides', () => {
180181
});
181182

182183
describe('linked worktree detection', () => {
184+
test('normalizes Windows native realpath prefixes for comparison', () => {
185+
expect(normalizePathForComparison('\\\\?\\C:\\Temp\\Linked\\.git\\')).toBe(
186+
process.platform === 'win32' ? 'c:/temp/linked/.git' : 'C:/Temp/Linked/.git',
187+
);
188+
expect(normalizePathForComparison('\\\\?\\UNC\\server\\share\\linked\\.git')).toBe(
189+
'//server/share/linked/.git',
190+
);
191+
});
192+
183193
test('detects linked worktrees and symlinked directories inside them', () => {
184194
const fixture = createLinkedWorktreeFixture();
185195
const nested = join(fixture.linkedWorktree, 'nested');

0 commit comments

Comments
 (0)