DSH-Arch-Setup/plugins/dsh-touched-git/lib/git-write-scope.js
Шурупов Илья Викторович cf9443753e Add one-command Arch DSH setup
Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
2026-09-12 19:13:36 +03:00

138 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }
}