如何在提交-msg 钩子内提示用户?

我想警告用户,如果他们的提交消息没有遵循一定的指导原则,然后给他们选项来编辑他们的提交消息,忽略警告,或取消提交。问题是我似乎无法访问 stdin

下面是我的 commit-msg 文件:

function verify_info {
if [ -z "$(grep '$2:.*[a-zA-Z]' $1)" ]
then
echo >&2 $2 information should not be omitted
local_editor=`git config --get core.editor`
if [ -z "${local_editor}" ]
then
local_editor=${EDITOR}
fi
echo "Do you want to"
select CHOICE in "edit the commit message" "ignore this warning" "cancel the commit"; do
case ${CHOICE} in
i*) echo "Warning ignored"
;;
e*) ${local_editor} $1
verify_info "$1" $2
;;
*)  echo "CHOICE = ${CHOICE}"
exit 1
;;
esac
done
fi
}


verify_info "$1" "Scope"
if [ $# -ne 0 ];
then
exit $#
fi
verify_info "$1" "Affects"
if [ $# -ne 0 ];
then
exit $#
fi


exit 0

下面是保留 Scope 信息空白时的输出:

Scope information should not be omitted
Do you want to:
1) edit the commit message  3) cancel the commit
2) ignore this warning
#?

消息是正确的,但它实际上并没有停止输入。我还尝试使用更简单的 read命令,它也有同样的问题。看起来问题在于此时 git 已经控制了 stdin并提供了自己的输入。我该怎么补救?

更新: 似乎这可能是一个复制的 这个问题,不幸似乎表明我的运气。

25102 次浏览

The commit-msg hook is not run in an interactive environment (as you have noticed).

The only way to reliable notify the user would be to write an error to stdout, place a copy of the commit message in a BAD_MSG file and instruct the user to edit the file and git commit --file=BAD_MSG


If you have some control over the environment you could have an alternate editor which is a wrapper script that checks the proposed message, and can restart the editor with an extra commented message.

Basically, you run the editor, check the file saved against your rules. and if it fails, prepend your warning message (with leading #) to the file and restart the editor.

You could even allow them to put in a #FORCE=true line in the message which would suppress the check and continue.

Calling exec < /dev/tty assigns standard input to the keyboard. Works for me in a post-commit git hook:

#!/bin/sh


echo "[post-commit hook] Commit done!"


# Allows us to read user input below, assigns stdin to keyboard
exec < /dev/tty


while true; do
read -p "[post-commit hook] Check for outdated gems? (Y/n) " yn
if [ "$yn" = "" ]; then
yn='Y'
fi
case $yn in
[Yy] ) bundle outdated --pre; break;;
[Nn] ) exit;;
* ) echo "Please answer y or n for yes or no.";;
esac
done

To make select stop for input, you may also try to redirect the stdin of select from /dev/fd/3 (See: Read input in bash inside a while loop).

# sample code using a while loop to simulate git consuming stdin
{
echo 'fd 0' | while read -r stdin; do
echo "stdin: $stdin"
echo "Do you want to"
select CHOICE in "edit the commit message" "ignore this warning" "cancel the commit"; do
case ${CHOICE} in
i*) echo "Warning ignored"
;;
e*) echo ${local_editor} $1
echo verify_info "$1" $2
;;
*)  echo "CHOICE = ${CHOICE}"
exit 1
;;
esac
done 0<&3 3<&-
done
} 3<&- 3<&0

this works fine when running git commit from command line. On windows (haven't tryed on linux) if you use gitk or git-gui you won't be able to prompt because you get an error on the exec < /dev/tty line.

The sollution is to call git-bash.exe in your hook:

.git/hooks/post-commit contains:

#!/bin/sh
exec /c/Program\ Files/Git/git-bash.exe /path/to/my_repo/.git/hooks/post-checkout.sh

the .git/hooks/post-commit.sh file contains:

# --------------------------------------------------------
# usage: f_askContinue "my question ?"
function f_askContinue {
local myQuestion=$1


while true; do
read -p "${myQuestion} " -n 1 -r answer
case $answer in
[Yy]* ) printf "\nOK\n"; break;;
[Nn]* )   printf "\nAbandon\n";
exit;;
* ) printf "\nAnswer with Yes or No.\n";;
esac
done
}


f_askContinue "Do you want to continue ?"
echo "This command is executed after the prompt !"
read -p "Question? [y|n] " -n 1 -r < /dev/tty
echo
if echo $REPLY | grep -E '^[Yy]$' > /dev/null; then
#do if Yes
else
#do if No
fi

How to do it in Node.js or TypeScript

EDIT: I made an npm package


I see people commenting on how to do it for other languages in Eliot Sykes answer, but the JavaScript solution is a bit long so I'll make a separate answer.

I'm not sure if O_NOCTTY is required, but it doesn't seem to affect anything. I don't really understand what a controlling terminal is. GNU docs description. I think what it means is that with O_NOCTTY on, you wouldn't be able to send a CTRL+C to the process (if it doesn't already have a controlling terminal). In that case, I'll leave it on so you don't control spawned processes. The main node process should already have a controlling terminal, I think.

I adapted the answer from this GitHub issue

I don't see any docs on how to use the tty.ReadStream constructor so I did a bit of trial and error / digging through Node.js source code.

You have to use Object.defineProperty because Node.js internals uses it too, and doesn't define a setter. An alternative is to do process.stdin.fd = fd, but I get duplicate output that way.

Anyway, I wanted to use this with Husky.js and it seems to work so far. I should probably turn this into an npm package when I get the time.

Node.js

#!/usr/bin/env node


const fs = require('fs');
const tty = require('tty');


if (!process.stdin.isTTY) {
const { O_RDONLY, O_NOCTTY } = fs.constants;
let fd;
try {
fd = fs.openSync('/dev/tty', O_RDONLY + O_NOCTTY);
} catch (error) {
console.error('Please push your code in a terminal.');
process.exit(1);
}


const stdin = new tty.ReadStream(fd);


Object.defineProperty(process, 'stdin', {
configurable: true,
enumerable: true,
get: () => stdin,
});
}


...Do your stuff...


process.stdin.destroy();
process.exit(0);

TypeScript:

#!/usr/bin/env ts-node


import fs from 'fs';
import tty from 'tty';


if (!process.stdin.isTTY) {
const { O_RDONLY, O_NOCTTY } = fs.constants;
let fd;
try {
fd = fs.openSync('/dev/tty', O_RDONLY + O_NOCTTY);
} catch (error) {
console.error('Please push your code in a terminal.');
process.exit(1);
}


// @ts-ignore: `ReadStream` in @types/node incorrectly expects an object.
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/37174
const stdin = new tty.ReadStream(fd);


Object.defineProperty(process, 'stdin', {
configurable: true,
enumerable: true,
get: () => stdin,
});
}


...Do your stuff...


process.stdin.destroy();
process.exit(0);