From 98abfa27a5a1478a5ac660a52c078b1bf156ff27 Mon Sep 17 00:00:00 2001 From: Mukul Date: Thu, 25 Jun 2026 00:17:51 +0530 Subject: [PATCH] Handle vscode.git activation failure in VS Code forks The extension crashes in VS Code forks like Kiro and Cursor because activating the built-in vscode.git extension throws an unhandled error. This wraps the activation call in a try/catch and adds a fallback that reads .git/config directly to discover remotes when the git extension is unavailable. --- src/git/parseGitRemoteUrl.ts | 29 ++++++++++++++ src/git/repository.test.ts | 76 ++++++++++++++++++++++++++++++++++++ src/git/repository.ts | 72 +++++++++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/git/parseGitRemoteUrl.ts create mode 100644 src/git/repository.test.ts diff --git a/src/git/parseGitRemoteUrl.ts b/src/git/parseGitRemoteUrl.ts new file mode 100644 index 00000000..ca1bdee6 --- /dev/null +++ b/src/git/parseGitRemoteUrl.ts @@ -0,0 +1,29 @@ +/** + * Parse a git remote URL from a .git/config file content. + * Tries the specified remote name first, falls back to the first available remote. + */ +export function parseGitRemoteUrl(gitConfig: string, remoteName: string): string | undefined { + const remoteRegex = /\[remote "([^"]+)"\]\s*\n((?:\s+[^[].+\n?)*)/g; + const urlRegex = /\s*url\s*=\s*(.+)/; + + let fallbackUrl: string | undefined; + let match: RegExpExecArray | null; + + while ((match = remoteRegex.exec(gitConfig)) !== null) { + const name = match[1]; + const body = match[2]; + const urlMatch = body.match(urlRegex); + + if (urlMatch) { + const url = urlMatch[1].trim(); + if (name === remoteName) { + return url; + } + if (!fallbackUrl) { + fallbackUrl = url; + } + } + } + + return fallbackUrl; +} diff --git a/src/git/repository.test.ts b/src/git/repository.test.ts new file mode 100644 index 00000000..b3dfa90d --- /dev/null +++ b/src/git/repository.test.ts @@ -0,0 +1,76 @@ +import {parseGitRemoteUrl} from "./parseGitRemoteUrl"; + +describe("parseGitRemoteUrl", () => { + const sampleGitConfig = `[core] +\trepositoryformatversion = 0 +\tfilemode = true +[remote "origin"] +\turl = git@github.com:user/repo.git +\tfetch = +refs/heads/*:refs/remotes/origin/* +[remote "upstream"] +\turl = https://github.com/org/repo.git +\tfetch = +refs/heads/*:refs/remotes/upstream/* +[branch "main"] +\tremote = origin +\tmerge = refs/heads/main +`; + + it("should find the specified remote by name", () => { + expect(parseGitRemoteUrl(sampleGitConfig, "origin")).toBe("git@github.com:user/repo.git"); + }); + + it("should find a different remote by name", () => { + expect(parseGitRemoteUrl(sampleGitConfig, "upstream")).toBe("https://github.com/org/repo.git"); + }); + + it("should fall back to first remote if specified remote is not found", () => { + expect(parseGitRemoteUrl(sampleGitConfig, "nonexistent")).toBe("git@github.com:user/repo.git"); + }); + + it("should return undefined for empty config", () => { + expect(parseGitRemoteUrl("", "origin")).toBeUndefined(); + }); + + it("should return undefined for config with no remotes", () => { + const configWithoutRemotes = `[core] +\trepositoryformatversion = 0 +\tfilemode = true +[branch "main"] +\tremote = origin +\tmerge = refs/heads/main +`; + expect(parseGitRemoteUrl(configWithoutRemotes, "origin")).toBeUndefined(); + }); + + it("should handle HTTPS URLs", () => { + const config = `[remote "origin"] +\turl = https://github.com/owner/project.git +\tfetch = +refs/heads/*:refs/remotes/origin/* +`; + expect(parseGitRemoteUrl(config, "origin")).toBe("https://github.com/owner/project.git"); + }); + + it("should handle SSH URLs without .git suffix", () => { + const config = `[remote "origin"] +\turl = git@github.com:owner/project +\tfetch = +refs/heads/*:refs/remotes/origin/* +`; + expect(parseGitRemoteUrl(config, "origin")).toBe("git@github.com:owner/project"); + }); + + it("should handle enterprise GitHub URLs", () => { + const config = `[remote "origin"] +\turl = https://github.enterprise.com/org/repo.git +\tfetch = +refs/heads/*:refs/remotes/origin/* +`; + expect(parseGitRemoteUrl(config, "origin")).toBe("https://github.enterprise.com/org/repo.git"); + }); + + it("should handle config with spaces around equals sign", () => { + const config = `[remote "origin"] +\turl = git@github.com:user/repo.git +\tfetch = +refs/heads/*:refs/remotes/origin/* +`; + expect(parseGitRemoteUrl(config, "origin")).toBe("git@github.com:user/repo.git"); + }); +}); diff --git a/src/git/repository.ts b/src/git/repository.ts index e62253cc..a2a4b3b6 100644 --- a/src/git/repository.ts +++ b/src/git/repository.ts @@ -1,3 +1,6 @@ +import * as fs from "fs"; +import * as path from "path"; + import * as vscode from "vscode"; import {Octokit} from "@octokit/rest"; @@ -8,6 +11,7 @@ import {getGitHubApiUri, getRemoteName, useEnterprise} from "../configuration/co import {Protocol} from "../external/protocol"; import {logDebug, logError} from "../log"; import {API, GitExtension, RefType, RepositoryState} from "../typings/git"; +import {parseGitRemoteUrl} from "./parseGitRemoteUrl"; import {RepositoryPermission, getRepositoryPermission} from "./repository-permissions"; interface GitHubUrls { @@ -20,7 +24,14 @@ async function getGitExtension(): Promise { const gitExtension = vscode.extensions.getExtension("vscode.git"); if (gitExtension) { if (!gitExtension.isActive) { - await gitExtension.activate(); + try { + await gitExtension.activate(); + } catch (e) { + // In VS Code forks (Kiro, Cursor, etc.), activating built-in extensions + // may be restricted. Fall back gracefully. + logDebug("Unable to activate vscode.git extension, falling back to manual git detection"); + return undefined; + } } const git = gitExtension.exports.getAPI(1); @@ -127,9 +138,68 @@ export async function getGitHubUrls(): Promise { } } + // Fallback: When vscode.git is unavailable (e.g., in VS Code forks), + // try to discover GitHub repositories by reading .git/config directly + if (vscode.workspace.workspaceFolders) { + const fallbackUrls = getGitHubUrlsFromGitConfig(); + if (fallbackUrls && fallbackUrls.length > 0) { + return fallbackUrls; + } + } + return null; } +/** + * Fallback method to discover GitHub repository URLs by reading .git/config directly. + * Used when the vscode.git extension cannot be activated (e.g., in VS Code forks). + */ +function getGitHubUrlsFromGitConfig(): GitHubUrls[] | null { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return null; + } + + const results: GitHubUrls[] = []; + const remoteName = getRemoteName(); + + for (const folder of workspaceFolders) { + if (folder.uri.scheme !== "file") { + continue; + } + + try { + const gitConfigPath = path.join(folder.uri.fsPath, ".git", "config"); + const gitConfig = fs.readFileSync(gitConfigPath, "utf8"); + + // Parse remotes from git config + const url = parseGitRemoteUrl(gitConfig, remoteName); + if (!url) { + continue; + } + + const host = useEnterprise() ? new URL(getGitHubApiUri()).host : "github.com"; + + if ( + url.indexOf("github.com") !== -1 || + (useEnterprise() && url.indexOf(host) !== -1) || + url.indexOf(".ghe.com") !== -1 + ) { + results.push({ + workspaceUri: folder.uri, + url, + protocol: new Protocol(url) + }); + } + } catch { + // .git/config doesn't exist or can't be read — skip this folder + logDebug(`Could not read .git/config for workspace folder: ${folder.uri.fsPath}`); + } + } + + return results.length > 0 ? results : null; +} + export interface GitHubRepoContext { client: Octokit; repositoryState: RepositoryState | undefined;