You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
8.4 KiB

1 year ago
const fs = require('fs')
const path = require('path')
const VALID_GIT_HOOKS = [
'applypatch-msg',
'pre-applypatch',
'post-applypatch',
'pre-commit',
'pre-merge-commit',
'prepare-commit-msg',
'commit-msg',
'post-commit',
'pre-rebase',
'post-checkout',
'post-merge',
'pre-push',
'pre-receive',
'update',
'proc-receive',
'post-receive',
'post-update',
'reference-transaction',
'push-to-checkout',
'pre-auto-gc',
'post-rewrite',
'sendemail-validate',
'fsmonitor-watchman',
'p4-changelist',
'p4-prepare-changelist',
'p4-post-changelist',
'p4-pre-submit',
'post-index-change',
]
const VALID_OPTIONS = ['preserveUnused']
function getGitProjectRoot(directory=process.cwd()) {
let start = directory
if (typeof start === 'string') {
if (start[start.length - 1] !== path.sep) {
start += path.sep
}
start = path.normalize(start)
start = start.split(path.sep)
}
if (!start.length) {
return undefined
}
start.pop()
let dir = start.join(path.sep)
let fullPath = path.join(dir, '.git')
if (fs.existsSync(fullPath)) {
if(!fs.lstatSync(fullPath).isDirectory()) {
let content = fs.readFileSync(fullPath, { encoding: 'utf-8' })
let match = /^gitdir: (.*)\s*$/.exec(content)
if (match) {
return path.normalize(match[1])
}
}
return path.normalize(fullPath)
} else {
return getGitProjectRoot(start)
}
}
function getProjectRootDirectoryFromNodeModules(projectPath) {
function _arraysAreEqual(a1, a2) {
return JSON.stringify(a1) === JSON.stringify(a2)
}
const projDir = projectPath.split(/[\\/]/) // <- would split both on '/' and '\'
const indexOfPnpmDir = projDir.indexOf('.pnpm')
if (indexOfPnpmDir > -1) {
return projDir.slice(0, indexOfPnpmDir - 1).join('/');
}
// A yarn2 STAB
if (projDir.includes('.yarn') && projDir.includes('unplugged')) {
return undefined
}
if (projDir.length > 2 &&
_arraysAreEqual(projDir.slice(projDir.length - 2, projDir.length), [
'node_modules',
'simple-git-hooks'
])) {
return projDir.slice(0, projDir.length - 2).join('/')
}
return undefined
}
function checkSimpleGitHooksInDependencies(projectRootPath) {
if (typeof projectRootPath !== 'string') {
throw TypeError("Package json path is not a string!")
}
const {packageJsonContent} = _getPackageJson(projectRootPath)
// if simple-git-hooks in dependencies -> note user that he should remove move it to devDeps!
if ('dependencies' in packageJsonContent && 'simple-git-hooks' in packageJsonContent.dependencies) {
console.log('[WARN] You should move simple-git-hooks to the devDependencies!')
return true // We only check that we are in the correct package, e.g not in a dependency of a dependency
}
if (!('devDependencies' in packageJsonContent)) {
return false
}
return 'simple-git-hooks' in packageJsonContent.devDependencies
}
function setHooksFromConfig(projectRootPath=process.cwd(), argv=process.argv) {
const customConfigPath = _getCustomConfigPath(argv)
const config = _getConfig(projectRootPath, customConfigPath)
if (!config) {
throw('[ERROR] Config was not found! Please add `.simple-git-hooks.js` or `simple-git-hooks.js` or `.simple-git-hooks.json` or `simple-git-hooks.json` or `simple-git-hooks` entry in package.json.\r\nCheck README for details')
}
const preserveUnused = Array.isArray(config.preserveUnused) ? config.preserveUnused : config.preserveUnused ? VALID_GIT_HOOKS: []
for (let hook of VALID_GIT_HOOKS) {
if (Object.prototype.hasOwnProperty.call(config, hook)) {
_setHook(hook, config[hook], projectRootPath)
} else if (!preserveUnused.includes(hook)) {
_removeHook(hook, projectRootPath)
}
}
}
function _setHook(hook, command, projectRoot=process.cwd()) {
const gitRoot = getGitProjectRoot(projectRoot)
if (!gitRoot) {
console.log('[INFO] No `.git` root folder found, skipping')
return
}
const hookCommand = "#!/bin/sh\n" + command
const hookDirectory = gitRoot + '/hooks/'
const hookPath = path.normalize(hookDirectory + hook)
const normalizedHookDirectory = path.normalize(hookDirectory)
if (!fs.existsSync(normalizedHookDirectory)) {
fs.mkdirSync(normalizedHookDirectory)
}
fs.writeFileSync(hookPath, hookCommand)
fs.chmodSync(hookPath, 0o0755)
console.log(`[INFO] Successfully set the ${hook} with command: ${command}`)
}
function removeHooks(projectRoot=process.cwd()) {
for (let configEntry of VALID_GIT_HOOKS) {
_removeHook(configEntry, projectRoot)
}
}
function _removeHook(hook, projectRoot=process.cwd()) {
const gitRoot = getGitProjectRoot(projectRoot)
const hookPath = path.normalize(gitRoot + '/hooks/' + hook)
if (fs.existsSync(hookPath)) {
fs.unlinkSync(hookPath)
}
}
function _getPackageJson(projectPath = process.cwd()) {
if (typeof projectPath !== "string") {
throw TypeError("projectPath is not a string")
}
const targetPackageJson = path.normalize(projectPath + '/package.json')
if (!fs.statSync(targetPackageJson).isFile()) {
throw Error("Package.json doesn't exist")
}
const packageJsonDataRaw = fs.readFileSync(targetPackageJson)
return { packageJsonContent: JSON.parse(packageJsonDataRaw), packageJsonPath: targetPackageJson }
}
function _getCustomConfigPath(argv=[]) {
// We'll run as one of the following:
// npx simple-git-hooks ./config.js
// node path/to/simple-git-hooks/cli.js ./config.js
return argv[2] || ''
}
function _getConfig(projectRootPath, configFileName='') {
if (typeof projectRootPath !== 'string') {
throw TypeError("Check project root path! Expected a string, but got " + typeof projectRootPath)
}
// every function here should accept projectRootPath as first argument and return object
const sources = [
() => _getConfigFromFile(projectRootPath, '.simple-git-hooks.cjs'),
() => _getConfigFromFile(projectRootPath, '.simple-git-hooks.js'),
() => _getConfigFromFile(projectRootPath, 'simple-git-hooks.cjs'),
() => _getConfigFromFile(projectRootPath, 'simple-git-hooks.js'),
() => _getConfigFromFile(projectRootPath, '.simple-git-hooks.json'),
() => _getConfigFromFile(projectRootPath, 'simple-git-hooks.json'),
() => _getConfigFromPackageJson(projectRootPath),
]
// if user pass his-own config path prepend custom path before the default ones
if (configFileName) {
sources.unshift(() => _getConfigFromFile(projectRootPath, configFileName))
}
for (let executeSource of sources) {
let config = executeSource()
if (config && _validateHooks(config)) {
return config
}
else if (config && !_validateHooks(config)) {
throw('[ERROR] Config was not in correct format. Please check git hooks or options name')
}
}
return undefined
}
function _getConfigFromPackageJson(projectRootPath = process.cwd()) {
const {packageJsonContent} = _getPackageJson(projectRootPath)
const config = packageJsonContent['simple-git-hooks'];
return typeof config === 'string' ? _getConfig(config) : packageJsonContent['simple-git-hooks']
}
function _getConfigFromFile(projectRootPath, fileName) {
if (typeof projectRootPath !== "string") {
throw TypeError("projectRootPath is not a string")
}
if (typeof fileName !== "string") {
throw TypeError("fileName is not a string")
}
try {
const filePath = path.isAbsolute(fileName)
? fileName
: path.normalize(projectRootPath + '/' + fileName)
if (filePath === __filename) {
return undefined
}
return require(filePath) // handle `.js` and `.json`
} catch (err) {
return undefined
}
}
function _validateHooks(config) {
for (let hookOrOption in config) {
if (!VALID_GIT_HOOKS.includes(hookOrOption) && !VALID_OPTIONS.includes(hookOrOption)) {
return false
}
}
return true
}
module.exports = {
checkSimpleGitHooksInDependencies,
setHooksFromConfig,
getProjectRootDirectoryFromNodeModules,
getGitProjectRoot,
removeHooks,
}