Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
npm-debug.log
coverage
.nyc_output
.tern-port
38 changes: 36 additions & 2 deletions hook
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#!/usr/bin/env bash

#
# Defense-in-depth for magit (the .git/hooks wrapper unsets it too); ensures
# hooks invoked from emacs/magit behave the same as on the CLI.
# https://magit.vc/manual/magit/My-Git-hooks-work-on-the-command_002dline-but-not-inside-Magit.html
#
unset GIT_LITERAL_PATHSPECS

HAS_NODE=`which node 2> /dev/null || which nodejs 2> /dev/null || which iojs 2> /dev/null`

#
Expand Down Expand Up @@ -37,14 +44,41 @@ elif [[ -x "$LOCAL" ]]; then
BINARY="$LOCAL"
fi

#
# Run from the repository root so `require.resolve('pre-commit')` works for Yarn PnP,
# and GUI git clients that invoke hooks with an unexpected cwd still resolve deps.
#
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || REPO_ROOT=""
if [[ -n "$REPO_ROOT" ]]; then
cd "$REPO_ROOT" || exit 1
fi

#
# Resolve the entry point of the `pre-commit` package via Node so we work for
# Yarn Plug'n'Play, hoisted, and nested layouts. If the package cannot be
# resolved (e.g. the user switched to a branch with no `node_modules`, or
# uninstalled `pre-commit`) skip the hook with exit 0 instead of failing the
# commit -- a missing dev dependency must not block work.
#
RESOLVED=
RESOLVE_RC=1
if [[ -n "$BINARY" ]]; then
RESOLVED="$("$BINARY" -e "try { console.log(require.resolve('pre-commit')); } catch (e) { process.exit(2); }" 2>/dev/null)"
RESOLVE_RC=$?
fi

#
# Add --dry-run cli flag support so we can execute this hook without side effects
# and see if it works in the current environment
#
if [[ $* == *--dry-run* ]]; then
if [[ -z "$BINARY" ]]; then
if [[ -z "$BINARY" ]] || [[ "$RESOLVE_RC" -ne 0 ]] || [[ -z "$RESOLVED" ]]; then
exit 1
fi
else
"$BINARY" "$("$BINARY" -e "console.log(require.resolve('pre-commit'))")"
if [[ "$RESOLVE_RC" -ne 0 ]] || [[ -z "$RESOLVED" ]]; then
echo "pre-commit: 'pre-commit' package is not installed; skipping hooks (run \`npm install\` to re-enable)." >&2
exit 0
fi
exec "$BINARY" "$RESOLVED"
fi
22 changes: 18 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
'use strict';

//
// cross-spawn.spawnSync returns the same shape as child_process.spawnSync
// (`status`, not `code`).
//
function failedSpawn(result) {
if (!result) return true;
if (result.error) return true;
if (result.signal) return true;
return result.status !== 0;
}

var spawn = require('cross-spawn')
, which = require('which')
, path = require('path')
Expand Down Expand Up @@ -173,8 +184,8 @@ Hook.prototype.initialize = function initialize() {
this.root = this.exec(this.git, ['rev-parse', '--show-toplevel']);
this.status = this.exec(this.git, ['status', '--porcelain']);

if (this.status.code) return this.log(Hook.log.status, 0);
if (this.root.code) return this.log(Hook.log.root, 0);
if (failedSpawn(this.status)) return this.log(Hook.log.status, 0);
if (failedSpawn(this.root)) return this.log(Hook.log.root, 0);

this.status = this.status.stdout.toString().trim();
this.root = this.root.stdout.toString().trim();
Expand Down Expand Up @@ -229,8 +240,11 @@ Hook.prototype.run = function runner() {
env: process.env,
cwd: hooked.root,
stdio: [0, 1, 2]
}).once('close', function closed(code) {
if (code) return hooked.log(hooked.format(Hook.log.failure, script, code));
}).once('close', function closed(code, signal) {
var exitCode = typeof code === 'number' ? code : (signal ? 1 : 0);
if (exitCode !== 0) {
return hooked.log(hooked.format(Hook.log.failure, script, exitCode));
}

again(scripts);
});
Expand Down
113 changes: 72 additions & 41 deletions install.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ var fs = require('fs')
, path = require('path')
, os = require('os')
, hook = path.join(__dirname, 'hook')
, hookAbs = path.resolve(hook)
, root = path.resolve(__dirname, '..', '..')
, exists = fs.existsSync || path.existsSync;

//
// POSIX single-quoted string for embedding paths in generated shell scripts.
//
function shellSingleQuote(str) {
return '\'' + str.replace(/'/g, '\'\\\'\'') + '\'';
}

//
// Gather the location of the possible hidden .git directory, the hooks
// directory which contains all git hooks and the absolute location of the
Expand All @@ -19,41 +27,50 @@ var fs = require('fs')

var git = getGitFolderPath(root);

// Function to recursively finding .git folder
//
// Walk up from `currentPath` looking for `.git`. Returns the path to the `.git`
// entry as soon as one is found, regardless of whether it is a directory (the
// regular case) or a file (submodules, linked worktrees, where `.git` contains
// `gitdir: <path>`).
//
function getGitFolderPath(currentPath) {
var git = path.resolve(currentPath, '.git')

if (!exists(git) || !fs.lstatSync(git).isDirectory()) {
console.log('pre-commit:');
console.log('pre-commit: Not found .git folder in', git);

var newPath = path.resolve(currentPath, '..');

// Stop if we on top folder
if (currentPath === newPath) {
return null;
var git = path.resolve(currentPath, '.git');

if (exists(git)) {
var stat = fs.lstatSync(git);
if (stat.isDirectory() || stat.isFile()) {
console.log('pre-commit:');
console.log('pre-commit: Found .git in', git);
return git;
}

return getGitFolderPath(newPath);
}

console.log('pre-commit:');
console.log('pre-commit: Found .git folder in', git);
return git;
console.log('pre-commit: No .git found in', currentPath);

var newPath = path.resolve(currentPath, '..');
if (currentPath === newPath) return null;

return getGitFolderPath(newPath);
}

//
// Resolve git directory for submodules
// When `.git` is a file (submodules and linked worktrees) it contains a
// `gitdir: <path>` pointer to the real git directory. Paths inside that file
// are resolved relative to the directory containing the `.git` file, not
// relative to the package root, so we use `path.dirname(git)` as the base.
//
if (exists(git) && fs.lstatSync(git).isFile()) {
var gitinfo = fs.readFileSync(git).toString()
, gitdirmatch = /gitdir: (.+)/.exec(gitinfo)
, gitdir = gitdirmatch.length == 2 ? gitdirmatch[1] : null;

if (gitdir !== null) {
git = path.resolve(root, gitdir);
hooks = path.resolve(git, 'hooks');
precommit = path.resolve(hooks, 'pre-commit');
if (git && fs.lstatSync(git).isFile()) {
var gitinfo = fs.readFileSync(git, 'utf8')
, gitdirmatch = /^gitdir:\s*(.+)$/m.exec(gitinfo)
, gitdir = gitdirmatch ? gitdirmatch[1].trim() : null;

if (gitdir) {
git = path.resolve(path.dirname(git), gitdir);
} else {
console.log('pre-commit:');
console.log('pre-commit: .git file did not contain a gitdir pointer; aborting.');
return;
}
}

Expand Down Expand Up @@ -81,7 +98,7 @@ if (exists(precommit) && !fs.lstatSync(precommit).isSymbolicLink()) {
console.log('pre-commit:');
console.log('pre-commit: Detected an existing git pre-commit hook');
fs.writeFileSync(precommit +'.old', fs.readFileSync(precommit));
console.log('pre-commit: Old pre-commit hook backuped to pre-commit.old');
console.log('pre-commit: Old pre-commit hook backed up to pre-commit.old');
console.log('pre-commit:');
}

Expand All @@ -92,20 +109,34 @@ if (exists(precommit) && !fs.lstatSync(precommit).isSymbolicLink()) {
try { fs.unlinkSync(precommit); }
catch (e) {}

// Create generic precommit hook that launches this modules hook (as well
// as stashing - unstashing the unstaged changes)
// TODO: we could keep launching the old pre-commit scripts
var hookRelativeUnixPath = hook.replace(root, '.');

if(os.platform() === 'win32') {
hookRelativeUnixPath = hookRelativeUnixPath.replace(/[\\\/]+/g, '/');
// Delegate to this package's `hook` script using an absolute path so Yarn Plug'n'Play
// and other layouts without `node_modules/pre-commit` still work. The hook script
// changes to the git root before resolving `pre-commit` via Node.
//
var hookLauncher = hookAbs;
if (os.platform() === 'win32') {
hookLauncher = hookLauncher.replace(/\\/g, '/');
}

var precommitContent = '#!/usr/bin/env bash' + os.EOL
+ hookRelativeUnixPath + os.EOL
+ 'RESULT=$?' + os.EOL
+ '[ $RESULT -ne 0 ] && exit 1' + os.EOL
+ 'exit 0' + os.EOL;
//
// Generated wrapper:
// * Unsets GIT_LITERAL_PATHSPECS so hooks invoked from magit/emacs behave the
// same as on the command line. See:
// https://magit.vc/manual/magit/My-Git-hooks-work-on-the-command_002dline-but-not-inside-Magit.html
// * If the package's `hook` script is missing (e.g. user switched to a branch
// without `node_modules`, or removed the `pre-commit` package), skip
// silently with exit 0 so commits are not blocked.
//
var precommitContent = [
'#!/usr/bin/env bash',
'unset GIT_LITERAL_PATHSPECS',
'HOOK=' + shellSingleQuote(hookLauncher),
'if [ ! -f "$HOOK" ]; then',
' exit 0',
'fi',
'exec bash "$HOOK" "$@"',
''
].join(os.EOL);

//
// It could be that we do not have rights to this folder which could cause the
Expand All @@ -121,10 +152,10 @@ catch (e) {
console.error('pre-commit:');
}

try { fs.chmodSync(precommit, '777'); }
try { fs.chmodSync(precommit, 0o755); }
catch (e) {
console.error('pre-commit:');
console.error('pre-commit: chmod 0777 the pre-commit file in your .git/hooks folder because:');
console.error('pre-commit: chmod 0755 the pre-commit file in your .git/hooks folder because:');
console.error('pre-commit: '+ e.message);
console.error('pre-commit:');
}
Loading