diff --git a/packages/desktop/src-tauri/src/commands/git.rs b/packages/desktop/src-tauri/src/commands/git.rs index 6e094a6e..fe72c160 100644 --- a/packages/desktop/src-tauri/src/commands/git.rs +++ b/packages/desktop/src-tauri/src/commands/git.rs @@ -327,6 +327,124 @@ static DELETIONS_REGEX: LazyLock = // --- Helpers --- +fn metadata_is_socket(metadata: &std::fs::Metadata) -> bool { + #[cfg(unix)] + { + use std::os::unix::fs::FileTypeExt; + return metadata.file_type().is_socket(); + } + #[cfg(not(unix))] + { + false + } +} + +fn is_launchd_listeners_socket(path: &str) -> bool { + path.contains("/com.apple.launchd.") && path.ends_with("/Listeners") +} + +async fn run_gpgconf(args: &[&str]) -> Option> { + let candidates = [ + "gpgconf", + "/opt/homebrew/bin/gpgconf", + "/usr/local/bin/gpgconf", + ]; + for candidate in candidates { + info!("git: trying gpgconf at {}", candidate); + let output = Command::new(candidate) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output() + .await; + if let Ok(result) = output { + if result.status.success() { + info!("git: gpgconf succeeded at {}", candidate); + return Some(result.stdout); + } + } + } + None +} + +async fn resolve_ssh_auth_sock() -> Option { + let mut launchd_fallback: Option = None; + if let Ok(value) = std::env::var("SSH_AUTH_SOCK") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + if let Ok(metadata) = fs::metadata(trimmed).await { + if metadata_is_socket(&metadata) { + if is_launchd_listeners_socket(trimmed) { + info!( + "git: SSH_AUTH_SOCK points to launchd listeners: {}", + trimmed + ); + launchd_fallback = Some(trimmed.to_string()); + } else { + info!("git: using SSH_AUTH_SOCK from environment: {}", trimmed); + return Some(trimmed.to_string()); + } + } + } + } + } + + let home_dir = dirs::home_dir()?; + let gpg_agent_sock = home_dir.join(".gnupg").join("S.gpg-agent.ssh"); + if let Ok(metadata) = fs::metadata(&gpg_agent_sock).await { + if metadata_is_socket(&metadata) { + info!( + "git: using gpg-agent SSH socket: {}", + gpg_agent_sock.to_string_lossy() + ); + return Some(gpg_agent_sock.to_string_lossy().to_string()); + } + } + + let mut gpgconf_path: Option = None; + if let Some(stdout) = run_gpgconf(&["--list-dirs", "agent-ssh-socket"]).await { + let candidate = String::from_utf8_lossy(&stdout).trim().to_string(); + if !candidate.is_empty() { + let path = PathBuf::from(candidate); + info!("git: gpgconf reported SSH socket at {}", path.to_string_lossy()); + if let Ok(metadata) = fs::metadata(&path).await { + if metadata_is_socket(&metadata) { + info!("git: gpgconf socket exists and is a socket"); + return Some(path.to_string_lossy().to_string()); + } + } + gpgconf_path = Some(path); + } + } + + if gpgconf_path.is_some() { + info!("git: launching gpg-agent via gpgconf"); + let _ = run_gpgconf(&["--launch", "gpg-agent"]).await; + if let Some(stdout) = run_gpgconf(&["--list-dirs", "agent-ssh-socket"]).await { + let candidate = String::from_utf8_lossy(&stdout).trim().to_string(); + if !candidate.is_empty() { + let path = PathBuf::from(candidate); + info!("git: gpgconf retried SSH socket at {}", path.to_string_lossy()); + if let Ok(metadata) = fs::metadata(&path).await { + if metadata_is_socket(&metadata) { + info!("git: gpgconf socket exists and is a socket after launch"); + return Some(path.to_string_lossy().to_string()); + } + } + } + } + } + + if let Some(fallback) = launchd_fallback { + info!("git: falling back to launchd SSH_AUTH_SOCK: {}", fallback); + return Some(fallback); + } + + warn!("git: no SSH_AUTH_SOCK resolved"); + None +} + async fn run_git(args: &[&str], cwd: &Path) -> Result { run_git_with_allowed_exit(args, cwd, &[]).await } @@ -336,7 +454,10 @@ async fn run_git_with_allowed_exit( cwd: &Path, allowed_codes: &[i32], ) -> Result { - let output = Command::new("git") + info!("git: running command {:?}", args); + let ssh_auth_sock = resolve_ssh_auth_sock().await; + let mut command = Command::new("git"); + command .args(args) .current_dir(cwd) .stdin(Stdio::null()) @@ -344,7 +465,12 @@ async fn run_git_with_allowed_exit( .env("GIT_OPTIONAL_LOCKS", "0") .env("GIT_TERMINAL_PROMPT", "0") .env("GCM_INTERACTIVE", "Never") - .env("LC_ALL", "C") + .env("LC_ALL", "C"); + if let Some(sock) = ssh_auth_sock.as_deref() { + info!("git: setting SSH_AUTH_SOCK for command: {}", sock); + command.env("SSH_AUTH_SOCK", sock); + } + let output = command .output() .await .context("Failed to execute git command")?; @@ -368,18 +494,25 @@ async fn run_git_bytes_with_allowed_exit_timeout( allowed_codes: &[i32], timeout_ms: u64, ) -> Result> { + info!("git: running command {:?}", args); + let ssh_auth_sock = resolve_ssh_auth_sock().await; + let mut command = Command::new("git"); + command + .args(args) + .current_dir(cwd) + .env("GIT_OPTIONAL_LOCKS", "0") + .env("GIT_TERMINAL_PROMPT", "0") + .env("GCM_INTERACTIVE", "Never") + .env("LC_ALL", "C") + .stdin(Stdio::null()) + .kill_on_drop(true); + if let Some(sock) = ssh_auth_sock.as_deref() { + info!("git: setting SSH_AUTH_SOCK for command: {}", sock); + command.env("SSH_AUTH_SOCK", sock); + } let output = tokio::time::timeout( std::time::Duration::from_millis(timeout_ms), - Command::new("git") - .args(args) - .current_dir(cwd) - .env("GIT_OPTIONAL_LOCKS", "0") - .env("GIT_TERMINAL_PROMPT", "0") - .env("GCM_INTERACTIVE", "Never") - .env("LC_ALL", "C") - .stdin(Stdio::null()) - .kill_on_drop(true) - .output(), + command.output(), ) .await .map_err(|_| anyhow!("Git command timed out after {}ms", timeout_ms))? diff --git a/packages/vscode/src/bridge.ts b/packages/vscode/src/bridge.ts index 0dc0ab49..ab4cfa3e 100644 --- a/packages/vscode/src/bridge.ts +++ b/packages/vscode/src/bridge.ts @@ -1,7 +1,9 @@ import * as vscode from 'vscode'; import * as os from 'os'; import * as path from 'path'; -import { spawn } from 'child_process'; +import * as fs from 'fs'; +import { spawn, execFile } from 'child_process'; +import { promisify } from 'util'; import { type OpenCodeManager } from './opencode'; import { createAgent, createCommand, deleteAgent, deleteCommand, getAgentSources, getCommandSources, updateAgent, updateCommand, type AgentScope, type CommandScope, AGENT_SCOPE, COMMAND_SCOPE, discoverSkills, getSkillSources, createSkill, updateSkill, deleteSkill, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile, type SkillScope, SKILL_SCOPE, getProviderSources, removeProviderConfig } from './opencodeConfig'; import { getProviderAuth, removeProviderAuth } from './opencodeAuth'; @@ -88,6 +90,8 @@ export interface BridgeContext { const SETTINGS_KEY = 'openchamber.settings'; const CLIENT_RELOAD_DELAY_MS = 800; +const execFileAsync = promisify(execFile); +const gpgconfCandidates = ['gpgconf', '/opt/homebrew/bin/gpgconf', '/usr/local/bin/gpgconf']; const readSettings = (ctx?: BridgeContext) => { const stored = ctx?.context?.globalState.get>(SETTINGS_KEY) || {}; @@ -198,12 +202,79 @@ const persistSettings = async (changes: Record, ctx?: BridgeCon const normalizeFsPath = (value: string) => value.replace(/\\/g, '/'); -const execGit = async (args: string[], cwd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> => ( - new Promise((resolve) => { +const isSocketPath = async (candidate: string): Promise => { + if (!candidate) { + return false; + } + try { + const stat = await fs.promises.stat(candidate); + return typeof stat.isSocket === 'function' && stat.isSocket(); + } catch { + return false; + } +}; + +const resolveSshAuthSock = async (): Promise => { + const existing = (process.env.SSH_AUTH_SOCK || '').trim(); + if (existing) { + return existing; + } + + if (process.platform === 'win32') { + return undefined; + } + + const gpgSock = path.join(os.homedir(), '.gnupg', 'S.gpg-agent.ssh'); + if (await isSocketPath(gpgSock)) { + return gpgSock; + } + + const runGpgconf = async (args: string[]): Promise => { + for (const candidate of gpgconfCandidates) { + try { + const { stdout } = await execFileAsync(candidate, args); + return String(stdout || ''); + } catch { + continue; + } + } + return ''; + }; + + const candidate = (await runGpgconf(['--list-dirs', 'agent-ssh-socket'])).trim(); + if (candidate && await isSocketPath(candidate)) { + return candidate; + } + + if (candidate) { + await runGpgconf(['--launch', 'gpg-agent']); + const retried = (await runGpgconf(['--list-dirs', 'agent-ssh-socket'])).trim(); + if (retried && await isSocketPath(retried)) { + return retried; + } + } + + return undefined; +}; + +const buildGitEnv = async (): Promise => { + const env: NodeJS.ProcessEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' }; + if (!env.SSH_AUTH_SOCK || !env.SSH_AUTH_SOCK.trim()) { + const resolved = await resolveSshAuthSock(); + if (resolved) { + env.SSH_AUTH_SOCK = resolved; + } + } + return env; +}; + +const execGit = async (args: string[], cwd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> => { + const env = await buildGitEnv(); + return new Promise((resolve) => { const proc = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], - env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + env, }); let stdout = ''; @@ -224,8 +295,8 @@ const execGit = async (args: string[], cwd: string): Promise<{ stdout: string; s proc.on('error', (error) => { resolve({ stdout: '', stderr: error instanceof Error ? error.message : String(error), exitCode: 1 }); }); - }) -); + }); +}; const gitCheckIgnoreNames = async (cwd: string, names: string[]): Promise> => { if (names.length === 0) { diff --git a/packages/vscode/src/gitService.ts b/packages/vscode/src/gitService.ts index 3e003b3f..1878fccf 100644 --- a/packages/vscode/src/gitService.ts +++ b/packages/vscode/src/gitService.ts @@ -7,12 +7,83 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as os from 'os'; -import { spawn } from 'child_process'; +import * as fs from 'fs'; +import { spawn, execFile } from 'child_process'; +import { promisify } from 'util'; import type { API as GitAPI, Repository, GitExtension, Status } from './git.d'; let gitApi: GitAPI | null = null; let gitExtensionEnabled = false; +const execFileAsync = promisify(execFile); +const gpgconfCandidates = ['gpgconf', '/opt/homebrew/bin/gpgconf', '/usr/local/bin/gpgconf']; + +async function isSocketPath(candidate: string): Promise { + if (!candidate) { + return false; + } + try { + const stat = await fs.promises.stat(candidate); + return typeof stat.isSocket === 'function' && stat.isSocket(); + } catch { + return false; + } +} + +async function resolveSshAuthSock(): Promise { + const existing = (process.env.SSH_AUTH_SOCK || '').trim(); + if (existing) { + return existing; + } + + if (process.platform === 'win32') { + return undefined; + } + + const gpgSock = path.join(os.homedir(), '.gnupg', 'S.gpg-agent.ssh'); + if (await isSocketPath(gpgSock)) { + return gpgSock; + } + + const runGpgconf = async (args: string[]): Promise => { + for (const candidate of gpgconfCandidates) { + try { + const { stdout } = await execFileAsync(candidate, args); + return String(stdout || ''); + } catch { + continue; + } + } + return ''; + }; + + const candidate = (await runGpgconf(['--list-dirs', 'agent-ssh-socket'])).trim(); + if (candidate && await isSocketPath(candidate)) { + return candidate; + } + + if (candidate) { + await runGpgconf(['--launch', 'gpg-agent']); + const retried = (await runGpgconf(['--list-dirs', 'agent-ssh-socket'])).trim(); + if (retried && await isSocketPath(retried)) { + return retried; + } + } + + return undefined; +} + +async function buildGitEnv(): Promise { + const env: NodeJS.ProcessEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' }; + if (!env.SSH_AUTH_SOCK || !env.SSH_AUTH_SOCK.trim()) { + const resolved = await resolveSshAuthSock(); + if (resolved) { + env.SSH_AUTH_SOCK = resolved; + } + } + return env; +} + /** * Initialize the git extension API */ @@ -106,32 +177,33 @@ async function execGit(args: string[], cwd: string): Promise<{ stdout: string; s return new Promise((resolve) => { const normalizedCwd = normalizePath(cwd); const gitPath = gitApi?.git.path || 'git'; - - const proc = spawn(gitPath, args, { - cwd: normalizedCwd, - // Note: shell: true is intentionally omitted. Node.js spawn can find - // executables in PATH on Windows without shell mode, and using shell mode - // can cause issues when the git path contains spaces (e.g., "C:\Program Files\Git\...") - env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, - }); - let stdout = ''; - let stderr = ''; + buildGitEnv().then((env) => { + const proc = spawn(gitPath, args, { + cwd: normalizedCwd, + env, + }); - proc.stdout?.on('data', (data) => { - stdout += data.toString(); - }); + let stdout = ''; + let stderr = ''; - proc.stderr?.on('data', (data) => { - stderr += data.toString(); - }); + proc.stdout?.on('data', (data) => { + stdout += data.toString(); + }); - proc.on('close', (exitCode) => { - resolve({ stdout, stderr, exitCode: exitCode ?? 0 }); - }); + proc.stderr?.on('data', (data) => { + stderr += data.toString(); + }); - proc.on('error', (error) => { - resolve({ stdout: '', stderr: error.message, exitCode: 1 }); + proc.on('close', (exitCode) => { + resolve({ stdout, stderr, exitCode: exitCode ?? 0 }); + }); + + proc.on('error', (error) => { + resolve({ stdout: '', stderr: error.message, exitCode: 1 }); + }); + }).catch((error) => { + resolve({ stdout: '', stderr: error instanceof Error ? error.message : String(error), exitCode: 1 }); }); }); } diff --git a/packages/web/server/lib/git-service.js b/packages/web/server/lib/git-service.js index dfbf1678..2299d02a 100644 --- a/packages/web/server/lib/git-service.js +++ b/packages/web/server/lib/git-service.js @@ -7,6 +7,81 @@ import { promisify } from 'util'; const fsp = fs.promises; const execFileAsync = promisify(execFile); +const gpgconfCandidates = ['gpgconf', '/opt/homebrew/bin/gpgconf', '/usr/local/bin/gpgconf']; + +const isSocketPath = async (candidate) => { + if (!candidate || typeof candidate !== 'string') { + return false; + } + try { + const stat = await fsp.stat(candidate); + return typeof stat.isSocket === 'function' && stat.isSocket(); + } catch { + return false; + } +}; + +const resolveSshAuthSock = async () => { + const existing = (process.env.SSH_AUTH_SOCK || '').trim(); + if (existing) { + return existing; + } + + if (process.platform === 'win32') { + return null; + } + + const gpgSock = path.join(os.homedir(), '.gnupg', 'S.gpg-agent.ssh'); + if (await isSocketPath(gpgSock)) { + return gpgSock; + } + + const runGpgconf = async (args) => { + for (const candidate of gpgconfCandidates) { + try { + const { stdout } = await execFileAsync(candidate, args); + return String(stdout || ''); + } catch { + continue; + } + } + return ''; + }; + + const candidate = (await runGpgconf(['--list-dirs', 'agent-ssh-socket'])).trim(); + if (candidate && await isSocketPath(candidate)) { + return candidate; + } + + if (candidate) { + await runGpgconf(['--launch', 'gpg-agent']); + const retried = (await runGpgconf(['--list-dirs', 'agent-ssh-socket'])).trim(); + if (retried && await isSocketPath(retried)) { + return retried; + } + } + + return null; +}; + +const buildGitEnv = async () => { + const env = { ...process.env }; + if (!env.SSH_AUTH_SOCK || !env.SSH_AUTH_SOCK.trim()) { + const resolved = await resolveSshAuthSock(); + if (resolved) { + env.SSH_AUTH_SOCK = resolved; + } + } + return env; +}; + +const createGit = async (directory) => { + const env = await buildGitEnv(); + if (!directory) { + return simpleGit({ env }); + } + return simpleGit({ baseDir: normalizeDirectoryPath(directory), env }); +}; const normalizeDirectoryPath = (value) => { if (typeof value !== 'string') { @@ -96,7 +171,7 @@ export async function ensureOpenChamberIgnored(directory) { } export async function getGlobalIdentity() { - const git = simpleGit(); + const git = await createGit(); try { const userName = await git.getConfig('user.name', 'global').catch(() => null); @@ -119,7 +194,7 @@ export async function getGlobalIdentity() { } export async function getRemoteUrl(directory, remoteName = 'origin') { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const url = await git.remote(['get-url', remoteName]); @@ -130,7 +205,7 @@ export async function getRemoteUrl(directory, remoteName = 'origin') { } export async function getCurrentIdentity(directory) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { @@ -162,7 +237,7 @@ export async function getCurrentIdentity(directory) { } export async function hasLocalIdentity(directory) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const localName = await git.getConfig('user.name', 'local').catch(() => null); @@ -174,7 +249,7 @@ export async function hasLocalIdentity(directory) { } export async function setLocalIdentity(directory, profile) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { @@ -210,7 +285,7 @@ export async function setLocalIdentity(directory, profile) { export async function getStatus(directory) { const directoryPath = normalizeDirectoryPath(directory); - const git = simpleGit(directoryPath); + const git = await createGit(directoryPath); try { // Use -uall to show all untracked files individually, not just directories @@ -389,7 +464,7 @@ export async function getStatus(directory) { } export async function getDiff(directory, { path, staged = false, contextLines = 3 } = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const args = ['diff', '--no-color']; @@ -442,7 +517,7 @@ export async function getDiff(directory, { path, staged = false, contextLines = } export async function getRangeDiff(directory, { base, head, path, contextLines = 3 } = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); const baseRef = typeof base === 'string' ? base.trim() : ''; const headRef = typeof head === 'string' ? head.trim() : ''; if (!baseRef || !headRef) { @@ -475,7 +550,7 @@ export async function getRangeDiff(directory, { base, head, path, contextLines = } export async function getRangeFiles(directory, { base, head } = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); const baseRef = typeof base === 'string' ? base.trim() : ''; const headRef = typeof head === 'string' ? head.trim() : ''; if (!baseRef || !headRef) { @@ -529,7 +604,7 @@ export async function getFileDiff(directory, { path: filePath, staged = false } } const directoryPath = normalizeDirectoryPath(directory); - const git = simpleGit(directoryPath); + const git = await createGit(directoryPath); const isImage = isImageFile(filePath); const mimeType = isImage ? getImageMimeType(filePath) : null; @@ -587,7 +662,7 @@ export async function getFileDiff(directory, { path: filePath, staged = false } export async function revertFile(directory, filePath) { const directoryPath = normalizeDirectoryPath(directory); - const git = simpleGit(directoryPath); + const git = await createGit(directoryPath); const repoRoot = path.resolve(directoryPath); const absoluteTarget = path.resolve(repoRoot, filePath); @@ -652,7 +727,7 @@ export async function collectDiffs(directory, files = []) { } export async function pull(directory, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const result = await git.pull( @@ -675,7 +750,7 @@ export async function pull(directory, options = {}) { } export async function push(directory, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); const buildUpstreamOptions = (raw) => { if (Array.isArray(raw)) { @@ -759,7 +834,7 @@ export async function deleteRemoteBranch(directory, options = {}) { throw new Error('branch is required to delete remote branch'); } - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); const targetBranch = branch.startsWith('refs/heads/') ? branch.substring('refs/heads/'.length) : branch; @@ -775,7 +850,7 @@ export async function deleteRemoteBranch(directory, options = {}) { } export async function fetch(directory, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { await git.fetch( @@ -792,7 +867,7 @@ export async function fetch(directory, options = {}) { } export async function commit(directory, message, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { @@ -822,7 +897,7 @@ export async function commit(directory, message, options = {}) { } export async function getBranches(directory) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const result = await git.branch(); @@ -876,7 +951,7 @@ async function filterActiveRemoteBranches(git, remoteBranches) { } export async function createBranch(directory, branchName, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { await git.checkoutBranch(branchName, options.startPoint || 'HEAD'); @@ -888,7 +963,7 @@ export async function createBranch(directory, branchName, options = {}) { } export async function checkoutBranch(directory, branchName) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { await git.checkout(branchName); @@ -905,7 +980,7 @@ export async function getWorktrees(directory) { return []; } - const git = simpleGit(directoryPath); + const git = await createGit(directoryPath); try { const result = await git.raw(['worktree', 'list', '--porcelain']); @@ -944,7 +1019,7 @@ export async function getWorktrees(directory) { } export async function addWorktree(directory, worktreePath, branch, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const args = ['worktree', 'add']; @@ -976,7 +1051,7 @@ export async function addWorktree(directory, worktreePath, branch, options = {}) } export async function removeWorktree(directory, worktreePath, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const args = ['worktree', 'remove', worktreePath]; @@ -995,7 +1070,7 @@ export async function removeWorktree(directory, worktreePath, options = {}) { } export async function deleteBranch(directory, branch, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const branchName = branch.startsWith('refs/heads/') @@ -1011,7 +1086,7 @@ export async function deleteBranch(directory, branch, options = {}) { } export async function getLog(directory, options = {}) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const maxCount = options.maxCount || 50; @@ -1109,7 +1184,7 @@ export async function getLog(directory, options = {}) { } export async function isLinkedWorktree(directory) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { const [gitDir, gitCommonDir] = await Promise.all([ git.raw(['rev-parse', '--git-dir']).then((output) => output.trim()), @@ -1123,7 +1198,7 @@ export async function isLinkedWorktree(directory) { } export async function getCommitFiles(directory, commitHash) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { @@ -1206,7 +1281,7 @@ export async function getCommitFiles(directory, commitHash) { } export async function renameBranch(directory, oldName, newName) { - const git = simpleGit(normalizeDirectoryPath(directory)); + const git = await createGit(directory); try { // Use git branch -m command to rename the branch