From 8393605bd0583c8cfadae921e56eba6fd0049617 Mon Sep 17 00:00:00 2001 From: Nagi Daou Date: Wed, 29 Apr 2026 16:46:09 -0400 Subject: [PATCH] fix: resolve SCM repository lookup failures on Windows SUBST drives On Windows, `fs.realpath` does not resolve SUBST drive substitutions, but `git rev-parse --show-toplevel` does. This causes a permanent path mismatch between GitLens's canonical repo paths and VS Code SCM's SUBST-based paths, making SCM-dependent features (Generate Commit Message, etc.) silently fail. Detect SUBST drives in `findRepositoryUri` by comparing drive letters between the workspace URI and the git-reported repo path, then resolve the SUBST target via `realpath.native` on the drive root to populate `toCanonicalMap`/`fromCanonicalMap`. Use these mappings in `getScmRepository` and `getOrOpenScmRepository` to fall back to the SUBST path when the canonical path doesn't match any SCM repository. --- src/env/node/git/cliGitProvider.ts | 53 +++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/env/node/git/cliGitProvider.ts b/src/env/node/git/cliGitProvider.ts index cea86a13a9bb8..cf1362a236a49 100644 --- a/src/env/node/git/cliGitProvider.ts +++ b/src/env/node/git/cliGitProvider.ts @@ -1278,6 +1278,35 @@ export class GlCliGitProvider implements GlGitProvider { if (symlink != null) { this.toCanonicalMap.set(repoPath, Uri.file(symlink)); this.fromCanonicalMap.set(symlink, Uri.file(repoPath)); + } else if (isWindows && repoPath) { + // On Windows, `fs.realpath` does not resolve SUBST drive substitutions, but + // `git rev-parse --show-toplevel` does. Detect this by comparing drive letters + // between the workspace URI and the git-reported repo path, then resolve the + // SUBST target via `realpath.native` on the drive root to populate the + // canonical/symlink mappings so SCM repository lookups can succeed. + const uriDrive = uri.fsPath[0]?.toLowerCase(); + const repoDrive = repoPath[0]?.toLowerCase(); + if (uriDrive && repoDrive && uriDrive !== repoDrive) { + try { + const resolvedDrive = normalizePath( + await new Promise((resolve, reject) => + realpath.native(`${uriDrive}:\\`, { encoding: 'utf8' }, (err, p) => + err ? reject(err) : resolve(p), + ), + ), + ); + const drivePrefix = resolvedDrive.endsWith('/') ? resolvedDrive : `${resolvedDrive}/`; + if (repoPath.toLowerCase().startsWith(drivePrefix.toLowerCase())) { + const suffix = repoPath.substring(drivePrefix.length); + symlink = normalizePath(`${uriDrive}:/${suffix}`); + this.toCanonicalMap.set(repoPath, Uri.file(symlink)); + this.fromCanonicalMap.set(symlink, Uri.file(repoPath)); + scope?.debug(`SUBST drive detected; repoPath=${repoPath}, substPath=${symlink}`); + } + } catch (ex) { + scope?.warn(`Failed to resolve SUBST drive '${uriDrive}:'; ${ex}`); + } + } } } @@ -1513,7 +1542,17 @@ export class GlCliGitProvider implements GlGitProvider { const scope = getScopedLogger(); try { const gitApi = await this.getScmGitApi(); - return gitApi?.getRepository(Uri.file(repoPath)) ?? undefined; + if (gitApi == null) return undefined; + + let repo = gitApi.getRepository(Uri.file(repoPath)); + if (repo == null) { + // If the canonical path doesn't match, try the symlinked/SUBST path + const symlinkUri = this.toCanonicalMap.get(repoPath); + if (symlinkUri != null) { + repo = gitApi.getRepository(symlinkUri); + } + } + return repo ?? undefined; } catch (ex) { scope?.error(ex); return undefined; @@ -1542,11 +1581,15 @@ export class GlCliGitProvider implements GlGitProvider { const gitApi = await this.getScmGitApi(); if (gitApi == null) return undefined; + // If the canonical path doesn't match VS Code's SCM paths (e.g., SUBST drives), + // use the symlinked/SUBST path that VS Code's SCM actually references + const effectiveUri = this.toCanonicalMap.get(getBestPath(uri)) ?? uri; + // `getRepository` will return an opened repository that "contains" that path, so for nested repositories, we need to force the opening of the nested path, otherwise we will only get the root repository - let repo = gitApi.getRepository(uri); - if (repo == null || (repo != null && repo.rootUri.toString() !== uri.toString())) { + let repo = gitApi.getRepository(effectiveUri); + if (repo == null || (repo != null && repo.rootUri.toString() !== effectiveUri.toString())) { scope?.info( - `opening the SCM repository for '${uri.toString(true)}'${ + `opening the SCM repository for '${effectiveUri.toString(true)}'${ source != null ? ` (source=${source.source})` : '' }: ${ repo == null @@ -1554,7 +1597,7 @@ export class GlCliGitProvider implements GlGitProvider { : `existing, non-matching repository '${repo.rootUri.toString(true)}'` }`, ); - repo = await gitApi.openRepository?.(uri); + repo = await gitApi.openRepository?.(effectiveUri); } return repo ?? undefined; } catch (ex) {