UNPKG

@jsenv/git-hooks

Version:

Declare git hooks in your package.json.

1,225 lines (1,052 loc) 33.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var url = require('url'); var fs = require('fs'); require('crypto'); var path = require('path'); var util = require('util'); const ensureUrlTrailingSlash = url => { return url.endsWith("/") ? url : `${url}/`; }; const isFileSystemPath = value => { if (typeof value !== "string") { throw new TypeError(`isFileSystemPath first arg must be a string, got ${value}`); } if (value[0] === "/") return true; return startsWithWindowsDriveLetter(value); }; const startsWithWindowsDriveLetter = string => { const firstChar = string[0]; if (!/[a-zA-Z]/.test(firstChar)) return false; const secondChar = string[1]; if (secondChar !== ":") return false; return true; }; const fileSystemPathToUrl = value => { if (!isFileSystemPath(value)) { throw new Error(`received an invalid value for fileSystemPath: ${value}`); } return String(url.pathToFileURL(value)); }; const assertAndNormalizeDirectoryUrl = value => { let urlString; if (value instanceof URL) { urlString = value.href; } else if (typeof value === "string") { if (isFileSystemPath(value)) { urlString = fileSystemPathToUrl(value); } else { try { urlString = String(new URL(value)); } catch (e) { throw new TypeError(`directoryUrl must be a valid url, received ${value}`); } } } else { throw new TypeError(`directoryUrl must be a string or an url, received ${value}`); } if (!urlString.startsWith("file://")) { throw new Error(`directoryUrl must starts with file://, received ${value}`); } return ensureUrlTrailingSlash(urlString); }; const assertAndNormalizeFileUrl = (value, baseUrl) => { let urlString; if (value instanceof URL) { urlString = value.href; } else if (typeof value === "string") { if (isFileSystemPath(value)) { urlString = fileSystemPathToUrl(value); } else { try { urlString = String(new URL(value, baseUrl)); } catch (e) { throw new TypeError(`fileUrl must be a valid url, received ${value}`); } } } else { throw new TypeError(`fileUrl must be a string or an url, received ${value}`); } if (!urlString.startsWith("file://")) { throw new Error(`fileUrl must starts with file://, received ${value}`); } return urlString; }; const statsToType = stats => { if (stats.isFile()) return "file"; if (stats.isDirectory()) return "directory"; if (stats.isSymbolicLink()) return "symbolic-link"; if (stats.isFIFO()) return "fifo"; if (stats.isSocket()) return "socket"; if (stats.isCharacterDevice()) return "character-device"; if (stats.isBlockDevice()) return "block-device"; return undefined; }; const urlToFileSystemPath = fileUrl => { if (fileUrl[fileUrl.length - 1] === "/") { // remove trailing / so that nodejs path becomes predictable otherwise it logs // the trailing slash on linux but does not on windows fileUrl = fileUrl.slice(0, -1); } const fileSystemPath = url.fileURLToPath(fileUrl); return fileSystemPath; }; // https://github.com/coderaiser/cloudcmd/issues/63#issuecomment-195478143 // https://nodejs.org/api/fs.html#fs_file_modes // https://github.com/TooTallNate/stat-mode // cannot get from fs.constants because they are not available on windows const S_IRUSR = 256; /* 0000400 read permission, owner */ const S_IWUSR = 128; /* 0000200 write permission, owner */ const S_IXUSR = 64; /* 0000100 execute/search permission, owner */ const S_IRGRP = 32; /* 0000040 read permission, group */ const S_IWGRP = 16; /* 0000020 write permission, group */ const S_IXGRP = 8; /* 0000010 execute/search permission, group */ const S_IROTH = 4; /* 0000004 read permission, others */ const S_IWOTH = 2; /* 0000002 write permission, others */ const S_IXOTH = 1; const permissionsToBinaryFlags = ({ owner, group, others }) => { let binaryFlags = 0; if (owner.read) binaryFlags |= S_IRUSR; if (owner.write) binaryFlags |= S_IWUSR; if (owner.execute) binaryFlags |= S_IXUSR; if (group.read) binaryFlags |= S_IRGRP; if (group.write) binaryFlags |= S_IWGRP; if (group.execute) binaryFlags |= S_IXGRP; if (others.read) binaryFlags |= S_IROTH; if (others.write) binaryFlags |= S_IWOTH; if (others.execute) binaryFlags |= S_IXOTH; return binaryFlags; }; const writeFileSystemNodePermissions = async (source, permissions) => { const sourceUrl = assertAndNormalizeFileUrl(source); const sourcePath = urlToFileSystemPath(sourceUrl); let binaryFlags; if (typeof permissions === "object") { permissions = { owner: { read: getPermissionOrComputeDefault("read", "owner", permissions), write: getPermissionOrComputeDefault("write", "owner", permissions), execute: getPermissionOrComputeDefault("execute", "owner", permissions) }, group: { read: getPermissionOrComputeDefault("read", "group", permissions), write: getPermissionOrComputeDefault("write", "group", permissions), execute: getPermissionOrComputeDefault("execute", "group", permissions) }, others: { read: getPermissionOrComputeDefault("read", "others", permissions), write: getPermissionOrComputeDefault("write", "others", permissions), execute: getPermissionOrComputeDefault("execute", "others", permissions) } }; binaryFlags = permissionsToBinaryFlags(permissions); } else { binaryFlags = permissions; } return chmodNaive(sourcePath, binaryFlags); }; const chmodNaive = (fileSystemPath, binaryFlags) => { return new Promise((resolve, reject) => { fs.chmod(fileSystemPath, binaryFlags, error => { if (error) { reject(error); } else { resolve(); } }); }); }; const actionLevels = { read: 0, write: 1, execute: 2 }; const subjectLevels = { others: 0, group: 1, owner: 2 }; const getPermissionOrComputeDefault = (action, subject, permissions) => { if (subject in permissions) { const subjectPermissions = permissions[subject]; if (action in subjectPermissions) { return subjectPermissions[action]; } const actionLevel = actionLevels[action]; const actionFallback = Object.keys(actionLevels).find(actionFallbackCandidate => actionLevels[actionFallbackCandidate] > actionLevel && actionFallbackCandidate in subjectPermissions); if (actionFallback) { return subjectPermissions[actionFallback]; } } const subjectLevel = subjectLevels[subject]; // do we have a subject with a stronger level (group or owner) // where we could read the action permission ? const subjectFallback = Object.keys(subjectLevels).find(subjectFallbackCandidate => subjectLevels[subjectFallbackCandidate] > subjectLevel && subjectFallbackCandidate in permissions); if (subjectFallback) { const subjectPermissions = permissions[subjectFallback]; return action in subjectPermissions ? subjectPermissions[action] : getPermissionOrComputeDefault(action, subjectFallback, permissions); } return false; }; const isWindows = process.platform === "win32"; const readFileSystemNodeStat = async (source, { nullIfNotFound = false, followLink = true } = {}) => { if (source.endsWith("/")) source = source.slice(0, -1); const sourceUrl = assertAndNormalizeFileUrl(source); const sourcePath = urlToFileSystemPath(sourceUrl); const handleNotFoundOption = nullIfNotFound ? { handleNotFoundError: () => null } : {}; return readStat(sourcePath, { followLink, ...handleNotFoundOption, ...(isWindows ? { // Windows can EPERM on stat handlePermissionDeniedError: async error => { // unfortunately it means we mutate the permissions // without being able to restore them to the previous value // (because reading current permission would also throw) try { await writeFileSystemNodePermissions(sourceUrl, 0o666); const stats = await readStat(sourcePath, { followLink, ...handleNotFoundOption, // could not fix the permission error, give up and throw original error handlePermissionDeniedError: () => { throw error; } }); return stats; } catch (e) { // failed to write permission or readState, throw original error as well throw error; } } } : {}) }); }; const readStat = (sourcePath, { followLink, handleNotFoundError = null, handlePermissionDeniedError = null } = {}) => { const nodeMethod = followLink ? fs.stat : fs.lstat; return new Promise((resolve, reject) => { nodeMethod(sourcePath, (error, statsObject) => { if (error) { if (handlePermissionDeniedError && (error.code === "EPERM" || error.code === "EACCES")) { resolve(handlePermissionDeniedError(error)); } else if (handleNotFoundError && error.code === "ENOENT") { resolve(handleNotFoundError(error)); } else { reject(error); } } else { resolve(statsObject); } }); }); }; const createCancelError = reason => { const cancelError = new Error(`canceled because ${reason}`); cancelError.name = "CANCEL_ERROR"; cancelError.reason = reason; return cancelError; }; const isCancelError = value => { return value && typeof value === "object" && value.name === "CANCEL_ERROR"; }; const arrayWithout = (array, item) => { const arrayWithoutItem = []; let i = 0; while (i < array.length) { const value = array[i]; i++; if (value === item) { continue; } arrayWithoutItem.push(value); } return arrayWithoutItem; }; // https://github.com/tc39/proposal-cancellation/tree/master/stage0 const createCancellationSource = () => { let requested = false; let cancelError; let registrationArray = []; const cancel = reason => { if (requested) return; requested = true; cancelError = createCancelError(reason); const registrationArrayCopy = registrationArray.slice(); registrationArray.length = 0; registrationArrayCopy.forEach(registration => { registration.callback(cancelError); // const removedDuringCall = registrationArray.indexOf(registration) === -1 }); }; const register = callback => { if (typeof callback !== "function") { throw new Error(`callback must be a function, got ${callback}`); } const existingRegistration = registrationArray.find(registration => { return registration.callback === callback; }); // don't register twice if (existingRegistration) { return existingRegistration; } const registration = { callback, unregister: () => { registrationArray = arrayWithout(registrationArray, registration); } }; registrationArray = [registration, ...registrationArray]; return registration; }; const throwIfRequested = () => { if (requested) { throw cancelError; } }; return { token: { register, get cancellationRequested() { return requested; }, throwIfRequested }, cancel }; }; const catchCancellation = asyncFn => { return asyncFn().catch(error => { if (isCancelError(error)) { // it means consume of the function will resolve with a cancelError // but when you cancel it means you're not interested in the result anymore // thanks to this it avoid unhandledRejection return error; } throw error; }); }; const readDirectory = async (url, { emfileMaxWait = 1000 } = {}) => { const directoryUrl = assertAndNormalizeDirectoryUrl(url); const directoryPath = urlToFileSystemPath(directoryUrl); const startMs = Date.now(); let attemptCount = 0; const attempt = () => { return readdirNaive(directoryPath, { handleTooManyFilesOpenedError: async error => { attemptCount++; const nowMs = Date.now(); const timeSpentWaiting = nowMs - startMs; if (timeSpentWaiting > emfileMaxWait) { throw error; } return new Promise(resolve => { setTimeout(() => { resolve(attempt()); }, attemptCount); }); } }); }; return attempt(); }; const readdirNaive = (directoryPath, { handleTooManyFilesOpenedError = null } = {}) => { return new Promise((resolve, reject) => { fs.readdir(directoryPath, (error, names) => { if (error) { // https://nodejs.org/dist/latest-v13.x/docs/api/errors.html#errors_common_system_errors if (handleTooManyFilesOpenedError && (error.code === "EMFILE" || error.code === "ENFILE")) { resolve(handleTooManyFilesOpenedError(error)); } else { reject(error); } } else { resolve(names); } }); }); }; const { mkdir } = fs.promises; const writeDirectory = async (destination, { recursive = true, allowUseless = false } = {}) => { const destinationUrl = assertAndNormalizeDirectoryUrl(destination); const destinationPath = urlToFileSystemPath(destinationUrl); const destinationStats = await readFileSystemNodeStat(destinationUrl, { nullIfNotFound: true, followLink: false }); if (destinationStats) { if (destinationStats.isDirectory()) { if (allowUseless) { return; } throw new Error(`directory already exists at ${destinationPath}`); } const destinationType = statsToType(destinationStats); throw new Error(`cannot write directory at ${destinationPath} because there is a ${destinationType}`); } try { await mkdir(destinationPath, { recursive }); } catch (error) { if (allowUseless && error.code === "EEXIST") { return; } throw error; } }; const resolveUrl = (specifier, baseUrl) => { if (typeof baseUrl === "undefined") { throw new TypeError(`baseUrl missing to resolve ${specifier}`); } return String(new URL(specifier, baseUrl)); }; const removeFileSystemNode = async (source, { allowUseless = false, recursive = false, maxRetries = 3, retryDelay = 100, onlyContent = false } = {}) => { const sourceUrl = assertAndNormalizeFileUrl(source); const sourceStats = await readFileSystemNodeStat(sourceUrl, { nullIfNotFound: true, followLink: false }); if (!sourceStats) { if (allowUseless) { return; } throw new Error(`nothing to remove at ${urlToFileSystemPath(sourceUrl)}`); } // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_class_fs_stats // FIFO and socket are ignored, not sure what they are exactly and what to do with them // other libraries ignore them, let's do the same. if (sourceStats.isFile() || sourceStats.isSymbolicLink() || sourceStats.isCharacterDevice() || sourceStats.isBlockDevice()) { await removeNonDirectory(sourceUrl.endsWith("/") ? sourceUrl.slice(0, -1) : sourceUrl, { maxRetries, retryDelay }); } else if (sourceStats.isDirectory()) { await removeDirectory(ensureUrlTrailingSlash(sourceUrl), { recursive, maxRetries, retryDelay, onlyContent }); } }; const removeNonDirectory = (sourceUrl, { maxRetries, retryDelay }) => { const sourcePath = urlToFileSystemPath(sourceUrl); let retryCount = 0; const attempt = () => { return unlinkNaive(sourcePath, { ...(retryCount >= maxRetries ? {} : { handleTemporaryError: async () => { retryCount++; return new Promise(resolve => { setTimeout(() => { resolve(attempt()); }, retryCount * retryDelay); }); } }) }); }; return attempt(); }; const unlinkNaive = (sourcePath, { handleTemporaryError = null } = {}) => { return new Promise((resolve, reject) => { fs.unlink(sourcePath, error => { if (error) { if (error.code === "ENOENT") { resolve(); } else if (handleTemporaryError && (error.code === "EBUSY" || error.code === "EMFILE" || error.code === "ENFILE" || error.code === "ENOENT")) { resolve(handleTemporaryError(error)); } else { reject(error); } } else { resolve(); } }); }); }; const removeDirectory = async (rootDirectoryUrl, { maxRetries, retryDelay, recursive, onlyContent }) => { const visit = async sourceUrl => { const sourceStats = await readFileSystemNodeStat(sourceUrl, { nullIfNotFound: true, followLink: false }); // file/directory not found if (sourceStats === null) { return; } if (sourceStats.isFile() || sourceStats.isCharacterDevice() || sourceStats.isBlockDevice()) { await visitFile(sourceUrl); } else if (sourceStats.isSymbolicLink()) { await visitSymbolicLink(sourceUrl); } else if (sourceStats.isDirectory()) { await visitDirectory(`${sourceUrl}/`); } }; const visitDirectory = async directoryUrl => { const directoryPath = urlToFileSystemPath(directoryUrl); const optionsFromRecursive = recursive ? { handleNotEmptyError: async () => { await removeDirectoryContent(directoryUrl); await visitDirectory(directoryUrl); } } : {}; await removeDirectoryNaive(directoryPath, { ...optionsFromRecursive, // Workaround for https://github.com/joyent/node/issues/4337 ...(process.platform === "win32" ? { handlePermissionError: async error => { let openOrCloseError; try { const fd = fs.openSync(directoryPath); fs.closeSync(fd); } catch (e) { openOrCloseError = e; } if (openOrCloseError) { if (openOrCloseError.code === "ENOENT") { return; } console.error(`error while trying to fix windows EPERM: ${openOrCloseError.stack}`); throw error; } await removeDirectoryNaive(directoryPath, { ...optionsFromRecursive }); } } : {}) }); }; const removeDirectoryContent = async directoryUrl => { const names = await readDirectory(directoryUrl); await Promise.all(names.map(async name => { const url = resolveUrl(name, directoryUrl); await visit(url); })); }; const visitFile = async fileUrl => { await removeNonDirectory(fileUrl, { maxRetries, retryDelay }); }; const visitSymbolicLink = async symbolicLinkUrl => { await removeNonDirectory(symbolicLinkUrl, { maxRetries, retryDelay }); }; if (onlyContent) { await removeDirectoryContent(rootDirectoryUrl); } else { await visitDirectory(rootDirectoryUrl); } }; const removeDirectoryNaive = (directoryPath, { handleNotEmptyError = null, handlePermissionError = null } = {}) => { return new Promise((resolve, reject) => { fs.rmdir(directoryPath, (error, lstatObject) => { if (error) { if (handlePermissionError && error.code === "EPERM") { resolve(handlePermissionError(error)); } else if (error.code === "ENOENT") { resolve(); } else if (handleNotEmptyError && ( // linux os error.code === "ENOTEMPTY" || // SunOS error.code === "EEXIST")) { resolve(handleNotEmptyError(error)); } else { reject(error); } } else { resolve(lstatObject); } }); }); }; const isWindows$1 = process.platform === "win32"; const baseUrlFallback = fileSystemPathToUrl(process.cwd()); const ensureParentDirectories = async destination => { const destinationUrl = assertAndNormalizeFileUrl(destination); const destinationPath = urlToFileSystemPath(destinationUrl); const destinationParentPath = path.dirname(destinationPath); return writeDirectory(destinationParentPath, { recursive: true, allowUseless: true }); }; const isWindows$2 = process.platform === "win32"; const addCallback = callback => { const triggerHangUpOrDeath = () => callback(); // SIGHUP http://man7.org/linux/man-pages/man7/signal.7.html process.once("SIGUP", triggerHangUpOrDeath); return () => { process.removeListener("SIGUP", triggerHangUpOrDeath); }; }; const SIGUPSignal = { addCallback }; const addCallback$1 = callback => { // SIGINT is CTRL+C from keyboard also refered as keyboard interruption // http://man7.org/linux/man-pages/man7/signal.7.html // may also be sent by vscode https://github.com/Microsoft/vscode-node-debug/issues/1#issuecomment-405185642 process.once("SIGINT", callback); return () => { process.removeListener("SIGINT", callback); }; }; const SIGINTSignal = { addCallback: addCallback$1 }; const addCallback$2 = callback => { if (process.platform === "win32") { console.warn(`SIGTERM is not supported on windows`); return () => {}; } const triggerTermination = () => callback(); // SIGTERM http://man7.org/linux/man-pages/man7/signal.7.html process.once("SIGTERM", triggerTermination); return () => { process.removeListener("SIGTERM", triggerTermination); }; }; const SIGTERMSignal = { addCallback: addCallback$2 }; let beforeExitCallbackArray = []; let uninstall; const addCallback$3 = callback => { if (beforeExitCallbackArray.length === 0) uninstall = install(); beforeExitCallbackArray = [...beforeExitCallbackArray, callback]; return () => { if (beforeExitCallbackArray.length === 0) return; beforeExitCallbackArray = beforeExitCallbackArray.filter(beforeExitCallback => beforeExitCallback !== callback); if (beforeExitCallbackArray.length === 0) uninstall(); }; }; const install = () => { const onBeforeExit = () => { return beforeExitCallbackArray.reduce(async (previous, callback) => { await previous; return callback(); }, Promise.resolve()); }; process.once("beforeExit", onBeforeExit); return () => { process.removeListener("beforeExit", onBeforeExit); }; }; const beforeExitSignal = { addCallback: addCallback$3 }; const addCallback$4 = (callback, { collectExceptions = false } = {}) => { if (!collectExceptions) { const exitCallback = () => { callback(); }; process.on("exit", exitCallback); return () => { process.removeListener("exit", exitCallback); }; } const { getExceptions, stop } = trackExceptions(); const exitCallback = () => { process.removeListener("exit", exitCallback); stop(); callback({ exceptionArray: getExceptions().map(({ exception, origin }) => { return { exception, origin }; }) }); }; process.on("exit", exitCallback); return () => { process.removeListener("exit", exitCallback); }; }; const trackExceptions = () => { let exceptionArray = []; const unhandledRejectionCallback = (unhandledRejection, promise) => { exceptionArray = [...exceptionArray, { origin: "unhandledRejection", exception: unhandledRejection, promise }]; }; const rejectionHandledCallback = promise => { exceptionArray = exceptionArray.filter(exceptionArray => exceptionArray.promise !== promise); }; const uncaughtExceptionCallback = (uncaughtException, origin) => { // since node 12.4 https://nodejs.org/docs/latest-v12.x/api/process.html#process_event_uncaughtexception if (origin === "unhandledRejection") return; exceptionArray = [...exceptionArray, { origin: "uncaughtException", exception: uncaughtException }]; }; process.on("unhandledRejection", unhandledRejectionCallback); process.on("rejectionHandled", rejectionHandledCallback); process.on("uncaughtException", uncaughtExceptionCallback); return { getExceptions: () => exceptionArray, stop: () => { process.removeListener("unhandledRejection", unhandledRejectionCallback); process.removeListener("rejectionHandled", rejectionHandledCallback); process.removeListener("uncaughtException", uncaughtExceptionCallback); } }; }; const exitSignal = { addCallback: addCallback$4 }; const addCallback$5 = callback => { return eventRace({ SIGHUP: { register: SIGUPSignal.addCallback, callback: () => callback("SIGHUP") }, SIGINT: { register: SIGINTSignal.addCallback, callback: () => callback("SIGINT") }, ...(process.platform === "win32" ? {} : { SIGTERM: { register: SIGTERMSignal.addCallback, callback: () => callback("SIGTERM") } }), beforeExit: { register: beforeExitSignal.addCallback, callback: () => callback("beforeExit") }, exit: { register: exitSignal.addCallback, callback: () => callback("exit") } }); }; const eventRace = eventMap => { const unregisterMap = {}; const unregisterAll = reason => { return Object.keys(unregisterMap).map(name => unregisterMap[name](reason)); }; Object.keys(eventMap).forEach(name => { const { register, callback } = eventMap[name]; unregisterMap[name] = register((...args) => { unregisterAll(); callback(...args); }); }); return unregisterAll; }; const teardownSignal = { addCallback: addCallback$5 }; const createCancellationTokenForProcess = () => { const teardownCancelSource = createCancellationSource(); teardownSignal.addCallback(reason => teardownCancelSource.cancel(`process ${reason}`)); return teardownCancelSource.token; }; const readFilePromisified = util.promisify(fs.readFile); const readFile = async value => { const fileUrl = assertAndNormalizeFileUrl(value); const filePath = urlToFileSystemPath(fileUrl); const buffer = await readFilePromisified(filePath); return buffer.toString(); }; const isWindows$3 = process.platform === "win32"; /* eslint-disable import/max-dependencies */ const isLinux = process.platform === "linux"; // linux does not support recursive option const { writeFile: writeFileNode } = fs.promises; const writeFile = async (destination, content = "") => { const destinationUrl = assertAndNormalizeFileUrl(destination); const destinationPath = urlToFileSystemPath(destinationUrl); try { await writeFileNode(destinationPath, content); } catch (error) { if (error.code === "ENOENT") { await ensureParentDirectories(destinationUrl); await writeFileNode(destinationPath, content); return; } throw error; } }; const LOG_LEVEL_OFF = "off"; const LOG_LEVEL_DEBUG = "debug"; const LOG_LEVEL_INFO = "info"; const LOG_LEVEL_WARN = "warn"; const LOG_LEVEL_ERROR = "error"; const createLogger = ({ logLevel = LOG_LEVEL_INFO } = {}) => { if (logLevel === LOG_LEVEL_DEBUG) { return { debug, info, warn, error }; } if (logLevel === LOG_LEVEL_INFO) { return { debug: debugDisabled, info, warn, error }; } if (logLevel === LOG_LEVEL_WARN) { return { debug: debugDisabled, info: infoDisabled, warn, error }; } if (logLevel === LOG_LEVEL_ERROR) { return { debug: debugDisabled, info: infoDisabled, warn: warnDisabled, error }; } if (logLevel === LOG_LEVEL_OFF) { return { debug: debugDisabled, info: infoDisabled, warn: warnDisabled, error: errorDisabled }; } throw new Error(`unexpected logLevel. --- logLevel --- ${logLevel} --- allowed log levels --- ${LOG_LEVEL_OFF} ${LOG_LEVEL_ERROR} ${LOG_LEVEL_WARN} ${LOG_LEVEL_INFO} ${LOG_LEVEL_DEBUG}`); }; const debug = console.debug; const debugDisabled = () => {}; const info = console.info; const infoDisabled = () => {}; const warn = console.warn; const warnDisabled = () => {}; const error = console.error; const errorDisabled = () => {}; const HOOK_NAMES = ["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", "post-receive", "post-update", "push-to-checkout", "pre-auto-gc", "post-rewrite", "sendemail-validate"]; const generateHookFileContent = hookCommand => `#!/bin/sh # Generated by @jsenv/git-hooks ${hookCommand}`; const hookIsGeneratedByUs = hookFileContent => { return hookFileContent.includes(`# Generated by @jsenv/git-hooks`); }; const isWindows$4 = process.platform === "win32"; // https://github.com/typicode/husky/blob/master/src/installer/getScript.ts const installGitHooks = async ({ cancellationToken = createCancellationTokenForProcess(), logLevel, projectDirectoryUrl, ci = process.env.CI }) => { return catchCancellation(async () => { const logger = createLogger({ logLevel }); if (ci) { logger.info(`ci -> skip installGitHooks`); return; } projectDirectoryUrl = assertAndNormalizeDirectoryUrl(projectDirectoryUrl); const packageJsonFileUrl = resolveUrl("package.json", projectDirectoryUrl); const packageJsonFileString = await readFile(packageJsonFileUrl); const packageJsonData = JSON.parse(packageJsonFileString); const { scripts = {} } = packageJsonData; await Promise.all(HOOK_NAMES.map(async hookName => { const hookScriptName = `git-hook-${hookName}`; const hookFileUrl = resolveUrl(`.git/hooks/${hookName}`, projectDirectoryUrl); const hookFileStats = await readFileSystemNodeStat(hookFileUrl, { nullIfNotFound: true }); const hookScriptPresence = hookScriptName in scripts; const hookFilePresence = Boolean(hookFileStats); cancellationToken.throwIfRequested(); if (hookFilePresence) { const hookFileContent = await readFile(hookFileUrl); if (!hookIsGeneratedByUs(hookFileContent)) { if (hookScriptPresence) { logger.info(` ignore ${hookScriptName} script because there is a git ${hookName} hook file not generated by us.`); } return; } if (hookScriptPresence) { const hookFileContentForScript = generateHookFileContent(scripts[hookScriptName]); if (hookFileContentForScript === hookFileContent) { logger.debug(` keep existing git ${hookName} hook file. --- file --- ${urlToFileSystemPath(hookFileUrl)}`); return; } logger.info(` overwrite git ${hookName} hook file. --- file --- ${urlToFileSystemPath(hookFileUrl)} --- previous file content --- ${hookFileContent} --- file content --- ${hookFileContentForScript}`); await writeHook(hookFileUrl, hookFileContentForScript); } else { logger.info(` remove git ${hookName} hook file. --- file --- ${urlToFileSystemPath(hookFileUrl)} --- file content --- ${hookFileContent}`); await removeFileSystemNode(hookFileUrl); } } else if (hookScriptPresence) { const hookFileContentForScript = generateHookFileContent(scripts[hookScriptName]); logger.info(` write git ${hookName} hook file. --- file --- ${urlToFileSystemPath(hookFileUrl)} --- file content --- ${hookFileContentForScript}`); await writeHook(hookFileUrl, hookFileContentForScript); } })); }).catch(e => { // this is required to ensure unhandledRejection will still // set process.exitCode to 1 marking the process execution as errored // preventing further command to run process.exitCode = 1; throw e; }); }; const writeHook = async (hookFileUrl, hookFileContent) => { await writeFile(hookFileUrl, hookFileContent); if (!isWindows$4) { await writeFileSystemNodePermissions(hookFileUrl, { owner: { read: true, write: true, execute: true }, group: { read: true, write: false, execute: true }, others: { read: true, write: false, execute: true } }); } }; const uninstallGitHooks = async ({ cancellationToken = createCancellationTokenForProcess(), logLevel, projectDirectoryUrl }) => { return catchCancellation(async () => { const logger = createLogger({ logLevel }); projectDirectoryUrl = assertAndNormalizeDirectoryUrl(projectDirectoryUrl); await Promise.all(HOOK_NAMES.map(async hookName => { const hookFileUrl = resolveUrl(`.git/hooks/${hookName}`, projectDirectoryUrl); logger.debug(`seach file for git ${hookName} hook at ${urlToFileSystemPath(hookFileUrl)}`); let hookFileContent; try { hookFileContent = await readFile(hookFileUrl); } catch (e) { if (e.code === "ENOENT") { logger.debug(`no file for git ${hookName} hook`); return; } throw e; } cancellationToken.throwIfRequested(); if (hookIsGeneratedByUs(hookFileContent)) { logger.info(`remove git ${hookName} hook file at ${urlToFileSystemPath(hookFileUrl)}`); await removeFileSystemNode(hookFileUrl); } else { logger.debug(`ignore git ${hookName} hook at ${urlToFileSystemPath(hookFileUrl)} because not generated by us.`); } })); }).catch(e => { // this is required to ensure unhandledRejection will still // set process.exitCode to 1 marking the process execution as errored // preventing further command to run process.exitCode = 1; throw e; }); }; exports.installGitHooks = installGitHooks; exports.uninstallGitHooks = uninstallGitHooks; //# sourceMappingURL=main.cjs.map