mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-09 00:16:57 -07:00
30e0ab102a
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
export function isErrnoException(e: unknown): e is NodeJS.ErrnoException {
|
|
return e instanceof Error && 'code' in e;
|
|
}
|
|
|
|
export function tryRealpath(p: string): string {
|
|
try {
|
|
return fs.realpathSync(p);
|
|
} catch (_e) {
|
|
if (isErrnoException(_e) && _e.code === 'ENOENT') {
|
|
const parentDir = path.dirname(p);
|
|
if (parentDir === p) {
|
|
return p;
|
|
}
|
|
return path.join(tryRealpath(parentDir), path.basename(p));
|
|
}
|
|
throw _e;
|
|
}
|
|
}
|
|
|
|
export function resolveGitWorktreePaths(workspacePath: string): {
|
|
worktreeGitDir?: string;
|
|
mainGitDir?: string;
|
|
} {
|
|
try {
|
|
const gitPath = path.join(workspacePath, '.git');
|
|
const gitStat = fs.lstatSync(gitPath);
|
|
if (gitStat.isFile()) {
|
|
const gitContent = fs.readFileSync(gitPath, 'utf8');
|
|
const match = gitContent.match(/^gitdir:\s+(.+)$/m);
|
|
if (match && match[1]) {
|
|
let worktreeGitDir = match[1].trim();
|
|
if (!path.isAbsolute(worktreeGitDir)) {
|
|
worktreeGitDir = path.resolve(workspacePath, worktreeGitDir);
|
|
}
|
|
const resolvedWorktreeGitDir = tryRealpath(worktreeGitDir);
|
|
|
|
// Security check: Verify the bidirectional link to prevent sandbox escape
|
|
let isValid = false;
|
|
try {
|
|
const backlinkPath = path.join(resolvedWorktreeGitDir, 'gitdir');
|
|
const backlink = fs.readFileSync(backlinkPath, 'utf8').trim();
|
|
// The backlink must resolve to the workspace's .git file
|
|
if (tryRealpath(backlink) === tryRealpath(gitPath)) {
|
|
isValid = true;
|
|
}
|
|
} catch (_e) {
|
|
// Fallback for submodules: check core.worktree in config
|
|
try {
|
|
const configPath = path.join(resolvedWorktreeGitDir, 'config');
|
|
const config = fs.readFileSync(configPath, 'utf8');
|
|
const match = config.match(/^\s*worktree\s*=\s*(.+)$/m);
|
|
if (match && match[1]) {
|
|
const worktreePath = path.resolve(
|
|
resolvedWorktreeGitDir,
|
|
match[1].trim(),
|
|
);
|
|
if (tryRealpath(worktreePath) === tryRealpath(workspacePath)) {
|
|
isValid = true;
|
|
}
|
|
}
|
|
} catch (_e2) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
if (!isValid) {
|
|
return {}; // Reject: valid worktrees/submodules must have a readable backlink
|
|
}
|
|
|
|
const mainGitDir = tryRealpath(
|
|
path.dirname(path.dirname(resolvedWorktreeGitDir)),
|
|
);
|
|
return {
|
|
worktreeGitDir: resolvedWorktreeGitDir,
|
|
mainGitDir: mainGitDir.endsWith('.git') ? mainGitDir : undefined,
|
|
};
|
|
}
|
|
}
|
|
} catch (_e) {
|
|
// Ignore if .git doesn't exist, isn't readable, etc.
|
|
}
|
|
return {};
|
|
}
|