fix(dev-infra): migrate copied worktree hooks
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { isAbsolute, join, resolve } from 'node:path'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
|
||||
const MINIMUM_GIT = [2, 26, 0]
|
||||
const HOOKS_DIRECTORY = 'dsh-hooks'
|
||||
@@ -437,6 +437,15 @@ function inspectOwnedHooksDirectory(hooksPath) {
|
||||
return { markerPath, ...marker }
|
||||
}
|
||||
|
||||
function isRegisteredOwnedHooksPath(commonDirectory, hooksPath) {
|
||||
const normalizedHooksPath = normalizedPath(hooksPath)
|
||||
const isRegistered = registeredWorktreeConfigPaths(commonDirectory).some(
|
||||
configPath => normalizedPath(join(dirname(configPath), HOOKS_DIRECTORY)) === normalizedHooksPath,
|
||||
)
|
||||
if (!isRegistered) return false
|
||||
return inspectOwnedHooksDirectory(hooksPath)?.hooksPath === hooksPath
|
||||
}
|
||||
|
||||
function ensureOwnedHooksDirectory(hooksPath) {
|
||||
const inspected = inspectOwnedHooksDirectory(hooksPath)
|
||||
if (inspected !== undefined) return inspected
|
||||
@@ -561,14 +570,22 @@ async function main() {
|
||||
'worktree core.hooksPath',
|
||||
)
|
||||
let ownedHooksDirectory
|
||||
let copiedWorktreePathIsOwned = false
|
||||
if (worktreePath !== undefined && worktreePath !== hooksPath) {
|
||||
ownedHooksDirectory = inspectOwnedHooksDirectory(hooksPath)
|
||||
if (ownedHooksDirectory === undefined || ownedHooksDirectory.hooksPath !== worktreePath) {
|
||||
const worktreePathIsRelocated = ownedHooksDirectory?.hooksPath === worktreePath
|
||||
copiedWorktreePathIsOwned = !worktreePathIsRelocated
|
||||
&& isRegisteredOwnedHooksPath(commonDirectory, worktreePath)
|
||||
if (!worktreePathIsRelocated && !copiedWorktreePathIsOwned) {
|
||||
refuseScopedHooksPath({ origin: `file:${worktreeConfigPath}`, scope: 'worktree', value: worktreePath })
|
||||
}
|
||||
}
|
||||
const directWorktreePathIsOwned = worktreePath !== undefined
|
||||
&& (worktreePath === hooksPath || ownedHooksDirectory?.hooksPath === worktreePath)
|
||||
&& (
|
||||
worktreePath === hooksPath
|
||||
|| ownedHooksDirectory?.hooksPath === worktreePath
|
||||
|| copiedWorktreePathIsOwned
|
||||
)
|
||||
const effectiveEntry = effectiveConfigEntry(root, 'core.hooksPath')
|
||||
if (effectiveEntry !== undefined) {
|
||||
const effectivePathIsOwned = effectiveEntry.scope === 'worktree'
|
||||
@@ -593,6 +610,7 @@ async function main() {
|
||||
worktreePath !== undefined
|
||||
&& worktreePath !== hooksPath
|
||||
&& ownedHooksDirectory.hooksPath !== worktreePath
|
||||
&& !copiedWorktreePathIsOwned
|
||||
) {
|
||||
throw new Error(`hooks directory ownership changed while relocating ${JSON.stringify(worktreePath)}`)
|
||||
}
|
||||
|
||||
@@ -262,6 +262,30 @@ describe('worktree-local Lefthook installer', () => {
|
||||
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
|
||||
})
|
||||
|
||||
it('replaces the owned hook path Git copies into a newly added worktree', async () => {
|
||||
const fixture = createFixture()
|
||||
const mainInstall = await runInstaller(fixture, fixture.main)
|
||||
expect(mainInstall.status, mainInstall.stderr).toBe(0)
|
||||
const mainHooks = hooksPath(fixture, fixture.main)
|
||||
const mainHookBefore = readFileSync(join(mainHooks, 'pre-commit'), 'utf8')
|
||||
const lateLinked = join(fixture.container, 'late-linked')
|
||||
git(fixture, fixture.main, ['worktree', 'add', '-b', 'late-linked', lateLinked])
|
||||
write(join(lateLinked, 'lefthook.yml'), 'late-linked-worktree-config\n')
|
||||
installFakeLefthook(lateLinked)
|
||||
expect(git(fixture, lateLinked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(mainHooks)
|
||||
|
||||
const linkedInstall = await runInstaller(fixture, lateLinked)
|
||||
|
||||
expect(linkedInstall.status, linkedInstall.stderr).toBe(0)
|
||||
const linkedHooks = hooksPath(fixture, lateLinked)
|
||||
expect(linkedHooks).not.toBe(mainHooks)
|
||||
expect(git(fixture, lateLinked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(linkedHooks)
|
||||
expect(readFileSync(join(linkedHooks, 'pre-commit'), 'utf8')).toContain(
|
||||
'# config=late-linked-worktree-config',
|
||||
)
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBefore)
|
||||
})
|
||||
|
||||
it('serializes concurrent installs and keeps repeated output stable', async () => {
|
||||
const fixture = createFixture()
|
||||
const delayed = { DSH_TEST_LEFTHOOK_DELAY_MS: '150' }
|
||||
@@ -509,6 +533,30 @@ describe('worktree-local Lefthook installer', () => {
|
||||
expect(git(fixture, fixture.linked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe('linked-custom-hooks')
|
||||
})
|
||||
|
||||
it('does not trust an ownership marker outside a registered worktree hook path', async () => {
|
||||
const fixture = createFixture()
|
||||
const mainInstall = await runInstaller(fixture, fixture.main)
|
||||
expect(mainInstall.status, mainInstall.stderr).toBe(0)
|
||||
const externalHooks = join(fixture.container, 'external-owned-hooks')
|
||||
write(
|
||||
join(externalHooks, '.dsh-lefthook-owned'),
|
||||
`${JSON.stringify({
|
||||
version: 1,
|
||||
owner: 'deepseek-harness worktree-local lefthook hooks',
|
||||
hooksPath: externalHooks,
|
||||
})}\n`,
|
||||
0o600,
|
||||
)
|
||||
git(fixture, fixture.linked, ['config', '--worktree', 'core.hooksPath', externalHooks])
|
||||
|
||||
const result = await runInstaller(fixture, fixture.linked)
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('worktree-scoped core.hooksPath')
|
||||
expect(git(fixture, fixture.linked, ['config', '--worktree', '--get', 'core.hooksPath'])).toBe(externalHooks)
|
||||
expect(existsSync(hooksPath(fixture, fixture.linked))).toBe(false)
|
||||
})
|
||||
|
||||
it('refuses to activate a sibling worktree dormant hook path', async () => {
|
||||
const fixture = createFixture()
|
||||
const linkedConfig = join(gitDirectory(fixture, fixture.linked), 'config.worktree')
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user