import { execFile } from 'node:child_process' import { lstat, readlink, realpath } from 'node:fs/promises' import { dirname, isAbsolute, join, parse, relative, sep } from 'node:path' import { promisify } from 'node:util' const execute = promisify(execFile) export class GitWriteScopeError extends Error { constructor(code, message) { super(message) this.name = 'GitWriteScopeError' this.code = code } } export async function canonicalWriteTarget(absolutePath, signal) { if (!isAbsolute(absolutePath)) throw new GitWriteScopeError('invalid-write-path', 'Git scope checks require an absolute file path.') let current = parse(absolutePath).root let components = absolutePath.slice(current.length).split(sep) let followedLinks = 0 while (components.length) { signal?.throwIfAborted() const component = components.shift() if (component === '' || component === '.') continue if (component === '..') { current = dirname(current) continue } const candidate = join(current, component) let info try { info = await lstat(candidate) } catch (error) { if (error.code !== 'ENOENT') throw error current = candidate continue } if (info.isSymbolicLink()) { if (++followedLinks > 40) throw new GitWriteScopeError('symlink-loop', 'Cannot determine Git scope through a symlink loop.') const link = await readlink(candidate) const root = parse(link).root if (root) current = root components = [...link.slice(root.length).split(sep), ...components] } else { current = candidate } } signal?.throwIfAborted() return current } async function existingParentDirectory(filePath, signal) { let directory = dirname(filePath) while (true) { signal?.throwIfAborted() try { const info = await lstat(directory) if (!info.isDirectory()) throw new GitWriteScopeError('invalid-write-parent', 'The write target has a non-directory parent.') return directory } catch (error) { if (error.code !== 'ENOENT') throw error const parent = dirname(directory) if (parent === directory) throw error directory = parent } } } function gitEnvironment() { return { ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_'))), LC_ALL: 'C' } } async function rejectUnresolvedGitMetadata(directory, signal) { let current = directory while (true) { signal?.throwIfAborted() try { await lstat(join(current, '.git')) throw new GitWriteScopeError('git-scope-unavailable', 'Git metadata exists but its repository could not be resolved. Fix it before writing.') } catch (error) { if (error.code !== 'ENOENT') throw error } const parent = dirname(current) if (parent === current) return current = parent } } export async function repositoryForWriteTarget(filePath, signal) { const canonicalPath = await canonicalWriteTarget(filePath, signal) const directory = await existingParentDirectory(canonicalPath, signal) let result try { result = await execute('git', ['-C', directory, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', env: gitEnvironment(), timeout: 10000, maxBuffer: 1024 * 1024, signal, }) } catch (error) { signal?.throwIfAborted() if (error.code === 128 && /^fatal: not a git repository \(or any /.test(error.stderr || '')) { await rejectUnresolvedGitMetadata(directory, signal) return { filePath: canonicalPath, repository: null } } throw new GitWriteScopeError('git-scope-unavailable', 'Write refused because Git scope could not be verified.') } signal?.throwIfAborted() const repository = await realpath(result.stdout.replace(/\r?\n$/, '')) const relativePath = relative(repository, canonicalPath) if (relativePath === '..' || relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(relativePath)) { throw new GitWriteScopeError('git-scope-mismatch', 'Git returned a repository that does not contain the physical write target.') } return { filePath: canonicalPath, repository } } export async function checkGitWriteScope({ filePath, sessionId, store, signal }) { signal?.throwIfAborted() if (!sessionId) throw new GitWriteScopeError('missing-write-session', 'An active session is required to check the write scope.') const target = await repositoryForWriteTarget(filePath, signal) if (!target.repository) { return { ...target, warning: `WARNING: ${target.filePath} is outside a Git repository. This write is not protected by Git history. Consider creating a Git repository for this directory. If this path should belong to an existing repository, verify the path and call add_touched_git_directory for that exact repository or worktree.` } } const entries = await store.list(sessionId) signal?.throwIfAborted() if (!entries.some(entry => entry.ok && entry.repository === target.repository)) { throw new GitWriteScopeError('unregistered-git-write', `Write blocked: ${target.repository} is not registered in this session’s Git Working Dirs menu. Call add_touched_git_directory for this repository or worktree before writing.`) } return { ...target, warning: null } }