diff --git a/src/commands/hook.ts b/src/commands/hook.ts index e3ee0ad0..d5699316 100644 --- a/src/commands/hook.ts +++ b/src/commands/hook.ts @@ -29,16 +29,16 @@ export default command({ parameters: [''], }, (argv) => { (async () => { - await assertGitRepo(); - + const gitRepoPath = await assertGitRepo(); const { installUninstall: mode } = argv._; - const hookExists = await fileExists(symlinkPath); + const absoltueSymlinkPath = path.join(gitRepoPath, symlinkPath); + const hookExists = await fileExists(absoltueSymlinkPath); if (mode === 'install') { if (hookExists) { // If the symlink is broken, it will throw an error // eslint-disable-next-line @typescript-eslint/no-empty-function - const realpath = await fs.realpath(symlinkPath).catch(() => {}); + const realpath = await fs.realpath(absoltueSymlinkPath).catch(() => {}); if (realpath === hookPath) { console.warn('The hook is already installed'); return; @@ -46,16 +46,16 @@ export default command({ throw new KnownError(`A different ${hookName} hook seems to be installed. Please remove it before installing aicommits.`); } - await fs.mkdir(path.dirname(symlinkPath), { recursive: true }); + await fs.mkdir(path.dirname(absoltueSymlinkPath), { recursive: true }); if (isWindows) { await fs.writeFile( - symlinkPath, + absoltueSymlinkPath, windowsHook, ); } else { - await fs.symlink(hookPath, symlinkPath, 'file'); - await fs.chmod(symlinkPath, 0o755); + await fs.symlink(hookPath, absoltueSymlinkPath, 'file'); + await fs.chmod(absoltueSymlinkPath, 0o755); } console.log(`${green('✔')} Hook installed`); return; @@ -68,20 +68,20 @@ export default command({ } if (isWindows) { - const scriptContent = await fs.readFile(symlinkPath, 'utf8'); + const scriptContent = await fs.readFile(absoltueSymlinkPath, 'utf8'); if (scriptContent !== windowsHook) { console.warn('Hook is not installed'); return; } } else { - const realpath = await fs.realpath(symlinkPath); + const realpath = await fs.realpath(absoltueSymlinkPath); if (realpath !== hookPath) { console.warn('Hook is not installed'); return; } } - await fs.rm(symlinkPath); + await fs.rm(absoltueSymlinkPath); console.log(`${green('✔')} Hook uninstalled`); return; } diff --git a/src/utils/git.ts b/src/utils/git.ts index b908546f..b2026aac 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -2,11 +2,13 @@ import { execa } from 'execa'; import { KnownError } from './error.js'; export const assertGitRepo = async () => { - const { stdout } = await execa('git', ['rev-parse', '--is-inside-work-tree'], { reject: false }); + const { stdout, failed } = await execa('git', ['rev-parse', '--show-toplevel'], { reject: false }); - if (stdout !== 'true') { + if (failed) { throw new KnownError('The current directory must be a Git repository!'); } + + return stdout; }; const excludeFromDiff = (path: string) => `:(exclude)${path}`; diff --git a/tests/specs/git-hook.ts b/tests/specs/git-hook.ts index 004cb3a3..fa6b7f30 100644 --- a/tests/specs/git-hook.ts +++ b/tests/specs/git-hook.ts @@ -1,3 +1,4 @@ +import path from 'path'; import { testSuite, expect } from 'manten'; import { assertOpenAiToken, @@ -22,6 +23,25 @@ export default testSuite(({ describe }) => { await fixture.rm(); }); + test('installs from Git repo subdirectory', async () => { + const { fixture, aicommits } = await createFixture({ + ...files, + 'some-dir': { + 'file.txt': '', + }, + }); + await createGit(fixture.path); + + const { stdout } = await aicommits(['hook', 'install'], { + cwd: path.join(fixture.path, 'some-dir'), + }); + expect(stdout).toMatch('Hook installed'); + + expect(await fixture.exists('.git/hooks/prepare-commit-msg')).toBe(true); + + await fixture.rm(); + }); + test('Commits', async () => { const { fixture, aicommits } = await createFixture(files); const git = await createGit(fixture.path); diff --git a/tests/utils.ts b/tests/utils.ts index 8bf3d1ab..77e190eb 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -19,8 +19,8 @@ const createAicommits = (fixture: FsFixture) => { args?: string[], options?: Options, ) => execaNode(aicommitsPath, args, { - ...options, cwd: fixture.path, + ...options, extendEnv: false, env: { ...homeEnv,