@jsenv/git-hooks
Version:
Declare git hooks in your package.json.
1,225 lines (1,052 loc) • 33.7 kB
JavaScript
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
;