Skip to content

Commit

Permalink
feat(hook): Use applying git config to take submodules and worktrees …
Browse files Browse the repository at this point in the history
…into account

* feat(hook): Use applying git config to take submodules and worktrees into account

Closes #66 & closes #46

* test(coverage): Take lib/hook.template.raw into account for code coverage

* test(install): Ensure code coverage for 100% of all branches

Even in the improbable case, that a .git entry on the filesystem is present, but neither a
directory, nor a file.

* refactor(hook): Remove unnecessary IIFE & normalize return value
  • Loading branch information
ta2edchimp authored and Kent C. Dodds committed Apr 5, 2016
1 parent 0641b02 commit 48d4794
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 7 deletions.
44 changes: 40 additions & 4 deletions lib/hook.template.raw
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 26 additions & 2 deletions lib/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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' +
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
107 changes: 107 additions & 0 deletions test/hook.template.raw.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}))

})

})
41 changes: 41 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 48d4794

Please sign in to comment.