diff --git a/lib/hook.template.raw b/lib/hook.template.raw index f4ae332..5514667 100644 --- a/lib/hook.template.raw +++ b/lib/hook.template.raw @@ -1,15 +1,51 @@ #!/usr/bin/env node // {{generated_message}} -(function hookEntryPoint() { +const fs = require('fs') +const path = require('path') +const ghooks = getGhooksEntryPoint() + +checkForGHooks(ghooks) +require(ghooks)(__dirname, __filename) + +function getGhooksEntryPoint() { + const worktree = getWorkTree() + if (worktree) { + return path.resolve(__dirname, '../', worktree, 'node_modules', 'ghooks') + } + return 'ghooks' +} + +function checkForGHooks(ghooksPath) { try { - require('ghooks') + require(ghooksPath) } catch (e) { warnAboutGHooks() process.exit(1) } - require('ghooks')(__dirname, __filename) -})() +} + +function getWorkTree() { + try { + return getWorkTreeFromConfig(getConfigFileContent()) + } catch (e) { + return null + } +} + +function getConfigFileContent() { + const configFile = path.resolve(__dirname, '../config') + const fileStat = fs.statSync(configFile) + if (fileStat && fileStat.isFile()) { + return fs.readFileSync(configFile, 'utf8') + } + return '' +} + +function getWorkTreeFromConfig(configFileContent) { + const worktreeRegEx = /\[core\][^]{0,}worktree = ([^\n]{1,})[^]{0,}/ + return worktreeRegEx.test(configFileContent) ? configFileContent.replace(worktreeRegEx, '$1') : '' +} function warnAboutGHooks() { console.warn( // eslint-disable-line no-console diff --git a/lib/install.js b/lib/install.js index 05a888f..934478a 100644 --- a/lib/install.js +++ b/lib/install.js @@ -26,7 +26,7 @@ const hooks = [ function installHooks() { const gitRoot = findGitRoot() if (gitRoot) { - const hooksDir = resolve(gitRoot, '.git/hooks') + const hooksDir = resolve(gitRoot, 'hooks') hooks.forEach(install.bind(null, hooksDir)) } else { warnAboutGit() @@ -35,12 +35,36 @@ function installHooks() { function findGitRoot() { try { - return findup.sync(process.cwd(), '.git') + return getGitRoot() } catch (e) { return null } } +function getGitRoot() { + const gitRoot = findup.sync(process.cwd(), '.git') + const gitPath = resolve(gitRoot, '.git') + const fileStat = fs.statSync(gitPath) + return gitPathDir(gitPath, fileStat) || gitPathFile(gitPath, fileStat, gitRoot) +} + +function gitPathDir(gitPath, fileStat) { + return fileStat.isDirectory() ? gitPath : null +} + +function gitPathFile(gitPath, fileStat, gitRoot) { + return fileStat.isFile() ? parseGitFile(fileStat, gitPath, gitRoot) : null +} + +function parseGitFile(fileStat, gitPath, gitRoot) { + const gitDirRegex = /[^]{0,}gitdir: ([^\n]{1,})[^]{0,}/ + const gitFileContents = fs.readFileSync(gitPath, 'utf8') + if (gitDirRegex.test(gitFileContents)) { + return resolve(gitRoot, gitFileContents.replace(gitDirRegex, '$1')) + } + return null +} + function warnAboutGit() { console.warn( // eslint-disable-line no-console 'This does not seem to be a git project.\n' + diff --git a/package.json b/package.json index deb8181..a4edc8b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint": "eslint bin/* lib/* test/", "test:unit": "mocha --compilers js:babel-register", "test": "npm run lint && npm run coverage", - "coverage": "istanbul cover -i lib/**/*.js _mocha -- --compilers js:babel-register test/**/*.test.js", + "coverage": "istanbul cover -i lib/**/* _mocha -- --compilers js:babel-register test/**/*.test.js", "check-coverage": "istanbul check-coverage --statements 100 --branches 100 --functions 100 --lines 100", "report-coverage": "cat ./coverage/lcov.info | codecov", "validate": "npm t && npm run check-coverage", diff --git a/test/hook.template.raw.test.js b/test/hook.template.raw.test.js index 397cb35..6e5aa2f 100644 --- a/test/hook.template.raw.test.js +++ b/test/hook.template.raw.test.js @@ -37,4 +37,111 @@ describe('hook.template.raw', function describeHookTemplateRaw() { }) + describe('when ghooks is installed, using worktree / in a submodule', () => { + + beforeEach(() => { + const path = require('path') + const worktree = '../../a/path/somewhere/else' + const ghooksResolved = path.resolve(process.cwd(), worktree, 'node_modules', 'ghooks') + const stub = { + fs: { + statSync: () => { + return {isFile: () => true} + }, + readFileSync: () => '[core]\n\tworktree = ' + worktree, + }, + } + stub[ghooksResolved] = this.ghooks = sinon.stub() + proxyquire('../lib/hook.template.raw', stub) + }) + + it('delegates the hook execution to ghooks', () => { + const dirname = process.cwd() + '/lib' + const filename = dirname + '/hook.template.raw' + expect(this.ghooks).to.have.been.calledWith(dirname, filename) + }) + + }) + + describe('when ghooks is not found, using worktree / in a submodule', () => { + + it('warns about ghooks not being found in gitdir', sinon.test(function test() { + const stub = { + ghooks: null, + fs: { + statSync: () => { + return {isFile: () => true} + }, + readFileSync: () => '[core]\n\tworktree = ../../a/path/somewhere/else', + }, + } + const warn = this.stub(console, 'warn') + const exitMessage = 'Exit process when ghooks not being present' + // instead of really exiting the process ... + const exit = this.stub(process, 'exit', () => { + // ... throw a predetermined exception, thus preventing + // further code execution within the tested module ... + throw Error(exitMessage) + }) + // ... and expect it to be eventually thrown + expect(() => { + proxyquire('../lib/hook.template.raw', stub) + }).to.throw(exitMessage) + expect(warn).to.have.been.calledWithMatch(/ghooks not found!/i) + expect(exit).to.have.been.calledWith(1) + })) + + it('warns about ghooks not being found due to no gitdir being present', sinon.test(function test() { + const stub = { + ghooks: null, + fs: { + statSync: () => { + return {isFile: () => true} + }, + readFileSync: () => '[anything]\n\tsomething = else', + }, + } + const warn = this.stub(console, 'warn') + const exitMessage = 'Exit process when ghooks not being present' + // instead of really exiting the process ... + const exit = this.stub(process, 'exit', () => { + // ... throw a predetermined exception, thus preventing + // further code execution within the tested module ... + throw Error(exitMessage) + }) + // ... and expect it to be eventually thrown + expect(() => { + proxyquire('../lib/hook.template.raw', stub) + }).to.throw(exitMessage) + expect(warn).to.have.been.calledWithMatch(/ghooks not found!/i) + expect(exit).to.have.been.calledWith(1) + })) + + it('warns about ghooks not being found due to no valid git config being present', sinon.test(function test() { + const stub = { + ghooks: null, + fs: { + statSync: () => { + return {isFile: () => false} + }, + }, + } + const warn = this.stub(console, 'warn') + const exitMessage = 'Exit process when ghooks not being present' + // instead of really exiting the process ... + const exit = this.stub(process, 'exit', () => { + // ... throw a predetermined exception, thus preventing + // further code execution within the tested module ... + throw Error(exitMessage) + }) + // ... and expect it to be eventually thrown + expect(() => { + proxyquire('../lib/hook.template.raw', stub) + }).to.throw(exitMessage) + expect(warn).to.have.been.calledWithMatch(/ghooks not found!/i) + expect(exit).to.have.been.calledWith(1) + })) + + }) + }) diff --git a/test/install.test.js b/test/install.test.js index 44cc7f7..65d7616 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -79,6 +79,47 @@ describe('install', function describeInstall() { }) +describe('install using worktree / as a submodule', function describeInstall() { + const install = require('../lib/install') + + it('warns when no gitdir specified for worktree / submodule', sinon.test(function test() { + fsStub({'.git': ''}) + const warn = this.stub(console, 'warn') + install() + expect(warn).to.have.been.calledWithMatch(/this does not seem to be a git project/i) + })) + + it('creates hooks directory using gitdir', () => { + fsStub({ + '.git': 'gitdir: ../../a/path/somewhere/else', + '../../a/path/somewhere/else': {}, + }) + install() + expect(fs.existsSync('../../a/path/somewhere/else/hooks')).to.be.true + }) + +}) + +describe('install (ensure 100% code coverage)', function describeInstall() { + const install = require('proxyquire')('../lib/install', { + fs: { + statSync() { + // to provoke the case where a '.git' entry on the filesystem + // is neither a directory nor a file + return {isDirectory: () => false, isFile: () => false} + }, + }, + }) + + it('warns when no gitdir specified for worktree / submodule', sinon.test(function test() { + const warn = this.stub(console, 'warn') + fsStub({'.git': ''}) + install() + expect(warn).to.have.been.calledWithMatch(/this does not seem to be a git project/i) + })) + +}) + function fileMode(file) { const allOn = 4095 // == 07777 (octal) return (fs.statSync(file).mode & allOn) // eslint-disable-line no-bitwise