fix(dev-infra): harden worktree hook ownership

This commit is contained in:
Tianyi Cui
2026-07-27 22:54:44 +08:00
parent 81b56a0d97
commit f7729b6f53
9 changed files with 342 additions and 47 deletions

View File

@@ -7,12 +7,15 @@ import { dirname, isAbsolute, join, resolve } from 'node:path'
const MINIMUM_GIT = [2, 26, 0]
const HOOKS_DIRECTORY = 'dsh-hooks'
const OWNERSHIP_MARKER = '.dsh-lefthook-owned'
const OWNERSHIP_MARKER_CONTENT = 'deepseek-harness worktree-local lefthook hooks\n'
const LEGACY_OWNERSHIP_MARKER_CONTENT = 'deepseek-harness worktree-local lefthook hooks\n'
const OWNERSHIP_MARKER_VERSION = 1
const OWNERSHIP_MARKER_OWNER = 'deepseek-harness worktree-local lefthook hooks'
const INSTALL_LOCK = 'dsh-lefthook-install.lock'
const INSTALL_LOCK_TIMEOUT_MS = 30_000
const INSTALL_LOCK_POLL_MS = 50
const ALLOW_HOOKS_PATH_OVERRIDE = 'DSH_LEFTHOOK_ALLOW_HOOKS_PATH_OVERRIDE'
const CONDITIONAL_INCLUDE_PATTERN = '^includeif\\..*\\.path$'
const REPOSITORY_EXTENSION_PATTERN = '^extensions\\.'
function errorCode(error) {
return typeof error === 'object' && error !== null && 'code' in error
@@ -177,17 +180,37 @@ function registeredWorktreeConfigPaths(commonDirectory) {
return paths
}
function assertDormantWorktreeConfigs(root, commonDirectory, commonConfigPath, currentConfigPath) {
if (worktreeConfigExtensionEnabled(root, commonConfigPath)) return
function lstatIfPresent(path) {
try {
return lstatSync(path)
} catch (error) {
if (errorCode(error) === 'ENOENT') return undefined
throw error
}
}
function assertCommonConfigFile(commonConfigPath) {
const configStat = lstatIfPresent(commonConfigPath)
if (configStat === undefined || !configStat.isFile() || configStat.isSymbolicLink()) {
throw new Error(
`refusing common repository config ${JSON.stringify(commonConfigPath)} because it is not a regular file`,
)
}
}
function assertWorktreeConfigFiles(root, commonDirectory, commonConfigPath, currentConfigPath) {
const extensionEnabled = worktreeConfigExtensionEnabled(root, commonConfigPath)
for (const configPath of registeredWorktreeConfigPaths(commonDirectory)) {
if (!existsSync(configPath)) continue
const configStat = lstatSync(configPath)
const configStat = lstatIfPresent(configPath)
if (configStat === undefined) continue
if (!configStat.isFile() || configStat.isSymbolicLink()) {
const state = extensionEnabled ? 'active' : 'dormant'
throw new Error(
`cannot enable extensions.worktreeConfig while dormant worktree config ${JSON.stringify(configPath)} `
+ 'is not a regular file; inspect it and enable the extension explicitly, or remove it, before retrying',
`refusing ${state} worktree config ${JSON.stringify(configPath)} because it is not a regular file; `
+ 'replace it with a regular worktree config or remove it before retrying',
)
}
if (extensionEnabled) continue
if (!hasDirectConfigEntries(root, configPath)) continue
const isCurrent = normalizedPath(configPath) === normalizedPath(currentConfigPath)
const owner = isCurrent ? 'current' : 'sibling'
@@ -259,7 +282,13 @@ function conditionalIncludeRisk(root, entry, inspect) {
return inspectConditionalConfig(root, target, inspect)
}
function migrationConfigSubject(root, configPath) {
function migrationConfigSubject(root, configPath, rejectRepositoryExtensions) {
if (rejectRepositoryExtensions) {
const extensionEntry = fileConfigMatchingEntries(root, configPath, REPOSITORY_EXTENSION_PATTERN)[0]
if (extensionEntry !== undefined) {
return `${extensionEntry.name} (${configSource(extensionEntry)})`
}
}
const worktreeEntry = fileConfigEntries(root, configPath, 'core.worktree')[0]
if (worktreeEntry !== undefined) return `core.worktree (${configSource(worktreeEntry)})`
const trueBareEntry = fileConfigEntries(root, configPath, 'core.bare')
@@ -272,7 +301,7 @@ function hooksPathConfigSubject(root, configPath) {
return entry === undefined ? undefined : `core.hooksPath (${configSource(entry)})`
}
function ensureWorktreeConfig(root, commonConfigPath) {
function planWorktreeConfigMigration(root, commonConfigPath) {
const versions = fileConfigValues(root, commonConfigPath, 'core.repositoryFormatVersion')
const versionText = assertSingle(versions, 'core.repositoryFormatVersion')
const version = Number(versionText)
@@ -280,6 +309,21 @@ function ensureWorktreeConfig(root, commonConfigPath) {
throw new Error(`unsupported core.repositoryFormatVersion: ${JSON.stringify(versionText)}`)
}
if (version === 0) {
const extensionEntry = fileConfigMatchingEntries(
root,
commonConfigPath,
REPOSITORY_EXTENSION_PATTERN,
)[0]
if (extensionEntry !== undefined) {
throw new Error(
`cannot upgrade core.repositoryFormatVersion from 0 while dormant repository extension `
+ `${extensionEntry.name} is configured (${configSource(extensionEntry)}); `
+ 'audit and migrate it, then set repository format 1 explicitly before retrying',
)
}
}
const extensionEnabled = worktreeConfigExtensionEnabled(root, commonConfigPath)
if (!extensionEnabled) {
@@ -287,7 +331,7 @@ function ensureWorktreeConfig(root, commonConfigPath) {
const risk = conditionalIncludeRisk(
root,
entry,
configPath => migrationConfigSubject(root, configPath),
configPath => migrationConfigSubject(root, configPath, version === 0),
)
if (risk !== undefined) {
const reason = risk.subject ?? risk.detail
@@ -318,6 +362,11 @@ function ensureWorktreeConfig(root, commonConfigPath) {
const directBareText = assertSingle(fileConfigValues(root, commonConfigPath, 'core.bare'), 'core.bare')
const directBare = directBareText === undefined ? undefined : parseGitBoolean(directBareText, 'core.bare')
return { directBare, extensionEnabled, version }
}
function applyWorktreeConfigMigration(root, commonConfigPath, migration) {
const { directBare, extensionEnabled, version } = migration
if (version === 0) {
git(['config', '--file', commonConfigPath, 'core.repositoryFormatVersion', '1'], root)
}
@@ -430,13 +479,38 @@ async function acquireInstallLock(commonDirectory) {
}
}
function ensureOwnedHooksDirectory(hooksPath) {
const markerPath = join(hooksPath, OWNERSHIP_MARKER)
if (!existsSync(hooksPath)) {
mkdirSync(hooksPath, { mode: 0o700 })
writeFileSync(markerPath, OWNERSHIP_MARKER_CONTENT, { flag: 'wx', mode: 0o600 })
return
function ownershipMarkerContent(hooksPath) {
return `${JSON.stringify({
version: OWNERSHIP_MARKER_VERSION,
owner: OWNERSHIP_MARKER_OWNER,
hooksPath,
})}\n`
}
function parseOwnershipMarker(content, hooksPath) {
if (content === LEGACY_OWNERSHIP_MARKER_CONTENT) return { hooksPath, legacy: true }
let parsed
try {
parsed = JSON.parse(content)
} catch {
return undefined
}
if (
typeof parsed !== 'object'
|| parsed === null
|| parsed.version !== OWNERSHIP_MARKER_VERSION
|| parsed.owner !== OWNERSHIP_MARKER_OWNER
|| typeof parsed.hooksPath !== 'string'
|| !isAbsolute(parsed.hooksPath)
) {
return undefined
}
return { hooksPath: parsed.hooksPath, legacy: false }
}
function inspectOwnedHooksDirectory(hooksPath) {
const markerPath = join(hooksPath, OWNERSHIP_MARKER)
if (!existsSync(hooksPath)) return undefined
const hooksStat = lstatSync(hooksPath)
if (!hooksStat.isDirectory() || hooksStat.isSymbolicLink()) {
throw new Error(`refusing to use non-directory or symlinked hooks path ${hooksPath}`)
@@ -445,9 +519,36 @@ function ensureOwnedHooksDirectory(hooksPath) {
throw new Error(`refusing to overwrite unowned hooks directory ${hooksPath}`)
}
const markerStat = lstatSync(markerPath)
if (!markerStat.isFile() || markerStat.isSymbolicLink() || readFileSync(markerPath, 'utf8') !== OWNERSHIP_MARKER_CONTENT) {
const marker = markerStat.isFile() && !markerStat.isSymbolicLink() && markerStat.nlink === 1
? parseOwnershipMarker(readFileSync(markerPath, 'utf8'), hooksPath)
: undefined
if (marker === undefined) {
throw new Error(`refusing to overwrite hooks directory with an invalid ownership marker: ${hooksPath}`)
}
for (const name of readdirSync(hooksPath)) {
if (name === OWNERSHIP_MARKER) continue
const entryPath = join(hooksPath, name)
const entryStat = lstatSync(entryPath)
if (!entryStat.isFile() || entryStat.isSymbolicLink() || entryStat.nlink !== 1) {
throw new Error(
`refusing to overwrite non-regular or multiply linked hook entry ${JSON.stringify(entryPath)}`,
)
}
}
return { markerPath, ...marker }
}
function ensureOwnedHooksDirectory(hooksPath) {
const inspected = inspectOwnedHooksDirectory(hooksPath)
if (inspected !== undefined) return inspected
mkdirSync(hooksPath, { mode: 0o700 })
const markerPath = join(hooksPath, OWNERSHIP_MARKER)
writeFileSync(markerPath, ownershipMarkerContent(hooksPath), { flag: 'wx', mode: 0o600 })
return { markerPath, hooksPath, legacy: false }
}
function updateOwnershipMarker(markerPath, hooksPath) {
writeFileSync(markerPath, ownershipMarkerContent(hooksPath), { mode: 0o600 })
}
function environmentWithoutCommandGitConfig() {
@@ -588,6 +689,13 @@ async function main() {
let installationError
try {
assertCommonConfigFile(commonConfigPath)
assertWorktreeConfigFiles(
root,
commonDirectory,
commonConfigPath,
worktreeConfigPath,
)
const worktreeEntries = fileConfigEntries(root, worktreeConfigPath, 'core.hooksPath')
const includedWorktreeEntry = worktreeEntries.find(
entry => !originIsFile(entry.origin, root, worktreeConfigPath),
@@ -599,14 +707,20 @@ async function main() {
worktreeEntries.map(entry => entry.value),
'worktree core.hooksPath',
)
let ownedHooksDirectory
if (worktreePath !== undefined && worktreePath !== hooksPath) {
refuseScopedHooksPath({ origin: `file:${worktreeConfigPath}`, scope: 'worktree', value: worktreePath })
ownedHooksDirectory = inspectOwnedHooksDirectory(hooksPath)
if (ownedHooksDirectory === undefined || ownedHooksDirectory.hooksPath !== worktreePath) {
refuseScopedHooksPath({ origin: `file:${worktreeConfigPath}`, scope: 'worktree', value: worktreePath })
}
}
const directWorktreePathIsOwned = worktreePath !== undefined
&& (worktreePath === hooksPath || ownedHooksDirectory?.hooksPath === worktreePath)
const effectiveEntry = effectiveConfigEntry(root, 'core.hooksPath')
if (effectiveEntry !== undefined) {
const effectivePathIsOwned = effectiveEntry.scope === 'worktree'
&& effectiveEntry.value === hooksPath
&& worktreePath === hooksPath
&& effectiveEntry.value === worktreePath
&& directWorktreePathIsOwned
&& originIsFile(effectiveEntry.origin, root, worktreeConfigPath)
if (!effectivePathIsOwned) {
if (effectiveEntry.scope === 'command' || effectiveEntry.scope === 'worktree') {
@@ -622,19 +736,21 @@ async function main() {
}
assertConditionalHooksPaths(root, worktreeConfigPath)
assertDormantWorktreeConfigs(
root,
commonDirectory,
commonConfigPath,
worktreeConfigPath,
)
ensureOwnedHooksDirectory(hooksPath)
ensureWorktreeConfig(root, commonConfigPath)
const migration = planWorktreeConfigMigration(root, commonConfigPath)
ownedHooksDirectory = ensureOwnedHooksDirectory(hooksPath)
if (
worktreePath !== undefined
&& worktreePath !== hooksPath
&& ownedHooksDirectory.hooksPath !== worktreePath
) {
throw new Error(`hooks directory ownership changed while relocating ${JSON.stringify(worktreePath)}`)
}
applyWorktreeConfigMigration(root, commonConfigPath, migration)
let pathChanged = false
try {
git(['config', '--worktree', 'core.hooksPath', hooksPath], root)
pathChanged = worktreePath === undefined
pathChanged = worktreePath !== hooksPath
const installedEntry = effectiveConfigEntry(root, 'core.hooksPath')
if (
installedEntry === undefined
@@ -645,9 +761,22 @@ async function main() {
throw new Error('new worktree-local core.hooksPath did not become the effective direct worktree value')
}
runLefthook(root, lefthook)
updateOwnershipMarker(ownedHooksDirectory.markerPath, hooksPath)
} catch (error) {
if (pathChanged) {
git(['config', '--worktree', '--unset-all', 'core.hooksPath'], root)
try {
if (worktreePath === undefined) {
git(['config', '--worktree', '--unset-all', 'core.hooksPath'], root)
} else {
git(['config', '--worktree', 'core.hooksPath', worktreePath], root)
}
} catch (rollbackError) {
throw new AggregateError(
[error, rollbackError],
`Lefthook installation failed: ${String(error)}; `
+ `worktree hook rollback also failed: ${String(rollbackError)}`,
)
}
}
throw error
}