From 225558394bf1d5d4a500d9e9cf1106acb5c24d57 Mon Sep 17 00:00:00 2001 From: hiroki osame Date: Sun, 2 Apr 2023 05:54:11 -0400 Subject: [PATCH] fix(hook): Windows support (#176) --- src/commands/hook.ts | 49 +++++++++++++++++++------ src/commands/prepare-commit-msg-hook.ts | 24 ++++++++++-- tests/index.ts | 1 + tests/specs/cli/commits.ts | 11 +----- tests/specs/git-hook.ts | 40 ++++++++++++++++++++ tests/utils.ts | 5 +++ 6 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 tests/specs/git-hook.ts diff --git a/src/commands/hook.ts b/src/commands/hook.ts index 309b5958..e3ee0ad0 100644 --- a/src/commands/hook.ts +++ b/src/commands/hook.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises'; import path from 'path'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; import { green, red } from 'kolorist'; import { command } from 'cleye'; import { assertGitRepo } from '../utils/git.js'; @@ -10,14 +10,24 @@ import { KnownError, handleCliError } from '../utils/error.js'; const hookName = 'prepare-commit-msg'; const symlinkPath = `.git/hooks/${hookName}`; -export const isCalledFromGitHook = process.argv[1].endsWith(`/${symlinkPath}`); +const hookPath = fileURLToPath(new URL('cli.mjs', import.meta.url)); + +export const isCalledFromGitHook = ( + process.argv[1] + .replace(/\\/g, '/') // Replace Windows back slashes with forward slashes + .endsWith(`/${symlinkPath}`) +); + +const isWindows = process.platform === 'win32'; +const windowsHook = ` +#!/usr/bin/env node +import(${JSON.stringify(pathToFileURL(hookPath))}) +`.trim(); export default command({ name: 'hook', parameters: [''], }, (argv) => { - const hookPath = fileURLToPath(new URL('cli.mjs', import.meta.url)); - (async () => { await assertGitRepo(); @@ -29,7 +39,7 @@ export default command({ // 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(() => {}); - if (realpath === hookPath) { + if (realpath === hookPath) { console.warn('The hook is already installed'); return; } @@ -37,8 +47,16 @@ export default command({ } await fs.mkdir(path.dirname(symlinkPath), { recursive: true }); - await fs.symlink(hookPath, symlinkPath, 'file'); - await fs.chmod(symlinkPath, 0o755); + + if (isWindows) { + await fs.writeFile( + symlinkPath, + windowsHook, + ); + } else { + await fs.symlink(hookPath, symlinkPath, 'file'); + await fs.chmod(symlinkPath, 0o755); + } console.log(`${green('✔')} Hook installed`); return; } @@ -48,10 +66,19 @@ export default command({ console.warn('Hook is not installed'); return; } - const realpath = await fs.realpath(symlinkPath); - if (realpath !== hookPath) { - console.warn('Hook is not installed'); - return; + + if (isWindows) { + const scriptContent = await fs.readFile(symlinkPath, 'utf8'); + if (scriptContent !== windowsHook) { + console.warn('Hook is not installed'); + return; + } + } else { + const realpath = await fs.realpath(symlinkPath); + if (realpath !== hookPath) { + console.warn('Hook is not installed'); + return; + } } await fs.rm(symlinkPath); diff --git a/src/commands/prepare-commit-msg-hook.ts b/src/commands/prepare-commit-msg-hook.ts index 8444d510..b2d02caf 100644 --- a/src/commands/prepare-commit-msg-hook.ts +++ b/src/commands/prepare-commit-msg-hook.ts @@ -50,14 +50,32 @@ export default () => (async () => { } finally { s.stop('Changes analyzed'); } + + /** + * When `--no-edit` is passed in, the base commit message is empty, + * and even when you use pass in comments via #, they are ignored. + * + * Note: `--no-edit` cannot be detected in argvs so this is the only way to check + */ + const baseMessage = await fs.readFile(messageFilePath, 'utf8'); + const supportsComments = baseMessage !== ''; const hasMultipleMessages = messages.length > 1; - let instructions = `# 🤖 AI generated commit${hasMultipleMessages ? 's' : ''}\n`; + + let instructions = ''; + + if (supportsComments) { + instructions = `# 🤖 AI generated commit${hasMultipleMessages ? 's' : ''}\n`; + } if (hasMultipleMessages) { - instructions += '# Select one of the following messages by uncommeting:\n'; + if (supportsComments) { + instructions += '# Select one of the following messages by uncommeting:\n'; + } instructions += `\n${messages.map(message => `# ${message}`).join('\n')}`; } else { - instructions += '# Edit the message below and commit:\n'; + if (supportsComments) { + instructions += '# Edit the message below and commit:\n'; + } instructions += `\n${messages[0]}\n`; } diff --git a/tests/index.ts b/tests/index.ts index e8aa8628..71c30867 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -3,4 +3,5 @@ import { describe } from 'manten'; describe('aicommits', ({ runTestSuite }) => { runTestSuite(import('./specs/cli/index.js')); runTestSuite(import('./specs/config.js')); + runTestSuite(import('./specs/git-hook.js')); }); diff --git a/tests/specs/cli/commits.ts b/tests/specs/cli/commits.ts index 98d473f4..efc70dc3 100644 --- a/tests/specs/cli/commits.ts +++ b/tests/specs/cli/commits.ts @@ -1,7 +1,5 @@ import { testSuite, expect } from 'manten'; -import { createFixture, createGit } from '../../utils.js'; - -const { OPENAI_KEY } = process.env; +import { createFixture, createGit, files } from '../../utils.js'; export default testSuite(({ describe }) => { if (process.platform === 'win32') { @@ -10,17 +8,12 @@ export default testSuite(({ describe }) => { return; } - if (!OPENAI_KEY) { + if (!process.env.OPENAI_KEY) { console.warn('⚠️ process.env.OPENAI_KEY is necessary to run these tests. Skipping...'); return; } describe('CLI', async ({ test, describe }) => { - const files = { - '.aicommits': `OPENAI_KEY=${OPENAI_KEY}`, - 'data.json': 'Lorem ipsum dolor sit amet '.repeat(10), - } as const; - test('Excludes files', async () => { const { fixture, aicommits } = await createFixture(files); const git = await createGit(fixture.path); diff --git a/tests/specs/git-hook.ts b/tests/specs/git-hook.ts new file mode 100644 index 00000000..d2c05bf2 --- /dev/null +++ b/tests/specs/git-hook.ts @@ -0,0 +1,40 @@ +import { testSuite, expect } from 'manten'; +import { createFixture, createGit, files } from '../utils.js'; + +export default testSuite(({ describe }) => { + describe('Git hook', ({ test }) => { + test('errors when not in Git repo', async () => { + const { fixture, aicommits } = await createFixture(files); + const { exitCode, stderr } = await aicommits(['hook', 'install'], { + reject: false, + }); + + expect(exitCode).toBe(1); + expect(stderr).toMatch('The current directory must be a Git repository'); + + await fixture.rm(); + }); + + test('Commits', async () => { + const { fixture, aicommits } = await createFixture(files); + const git = await createGit(fixture.path); + + const { stdout } = await aicommits(['hook', 'install']); + expect(stdout).toMatch('Hook installed'); + + await git('add', ['data.json']); + await git('commit', ['--no-edit'], { + env: { + HOME: fixture.path, + USERPROFILE: fixture.path, + }, + }); + + const { stdout: commitMessage } = await git('log', ['--pretty=%B']); + console.log('Committed with:', commitMessage); + expect(commitMessage.startsWith('# ')).not.toBe(true); + + await fixture.rm(); + }); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 46b580e5..ee5aeba8 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -72,3 +72,8 @@ export const createFixture = async ( aicommits, }; }; + +export const files = Object.freeze({ + '.aicommits': `OPENAI_KEY=${process.env.OPENAI_KEY}`, + 'data.json': 'Lorem ipsum dolor sit amet '.repeat(10), +});