fix(git): handle launchd SSH agent socket (#275)
* fix(git): propagate SSH_AUTH_SOCK to git commands * fix(git): ignore launchd SSH_AUTH_SOCK * fix(git): fallback to launchd SSH_AUTH_SOCK
This commit is contained in:
@@ -327,6 +327,124 @@ static DELETIONS_REGEX: LazyLock<Regex> =
|
||||
|
||||
// --- 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<Vec<u8>> {
|
||||
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<String> {
|
||||
let mut launchd_fallback: Option<String> = 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<PathBuf> = 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<String> {
|
||||
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<String> {
|
||||
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<Vec<u8>> {
|
||||
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))?
|
||||
|
||||
@@ -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<Record<string, unknown>>(SETTINGS_KEY) || {};
|
||||
@@ -198,12 +202,79 @@ const persistSettings = async (changes: Record<string, unknown>, 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<boolean> => {
|
||||
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<string | undefined> => {
|
||||
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<string> => {
|
||||
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<NodeJS.ProcessEnv> => {
|
||||
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<Set<string>> => {
|
||||
if (names.length === 0) {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<string | undefined> {
|
||||
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<string> => {
|
||||
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<NodeJS.ProcessEnv> {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user