86

I want to warn the user if their commit message doesn't follow a certain set of guidelines, and then give them the option to edit their commit message, ignore the warning, or cancel the commit. The problem is that I don't seem to have access to stdin.

Below is my commit-msg file:

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

Here is the output when I leave the Scope information blank:

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

The message is correct, but it doesn't actually stop for input. I've also tried using the simpler read command, and it has the same problem. It seems that the problem is that at this point git has control of stdin and is providing its own input. How do I fix this?

Update: It seems this might be a duplicate of this question which unfortunately seems to suggest I'm out of luck.

sRavioli
  • 27
  • 5
Ben Hocking
  • 7,790
  • 5
  • 37
  • 52
  • When you have access to a X Server you can escape to a graphical dialog tool. Ugly-but-works – Rudi Aug 06 '10 at 11:24
  • Instead of the error message you could simply provide an informative error message -- including echoing the necessary command to ignore the warning. – bstpierre Aug 10 '10 at 00:45
  • @btspierre, that's the approach I ended up taking. At the advice of John Feminella, I allowed the use of a environment variable to override the warning, and just echo the warning whenever a bad situation is encountered. – Ben Hocking Aug 10 '10 at 18:56
  • @Rudi: I'm not sure what you'd escape to the X Server, as git seems to have complete control of stdin. – Ben Hocking Aug 10 '10 at 18:57
  • I mean when you have a graphical user interface running you can "escape" from the terminal by running a graphical dialog box(like xdialog or kdialog), which is not bound to any tty. See http://techbase.kde.org/Development/Tutorials/Shell_Scripting_with_KDE_Dialogs or or http://xdialog.free.fr/ for further details. – Rudi Aug 13 '10 at 07:00
  • 1
    10 years later, this is being discussed: https://public-inbox.org/git/pull.790.git.1605625363309.gitgitgadget@gmail.com/T/#u – VonC Dec 29 '20 at 01:38

6 Answers6

181

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
sRavioli
  • 27
  • 5
Eliot Sykes
  • 9,616
  • 6
  • 50
  • 64
  • 24
    STDIN can be closed again with `exec <&-` – Andy Jan 24 '13 at 10:57
  • 16
    If you're using Ruby, that'd translate to `STDIN.reopen('/dev/tty')`. Awesome stuff, this is the real answer. – Matthew Scharley Jun 14 '13 at 09:39
  • 8
    This is awesome, but it may break when committing from another tool, such as an editor. Not sure how to get around that, but if someone has an idea, I'd be interested to hear. – Peeja Sep 17 '13 at 14:44
  • 3
    This even works in `bash` under [MingW32](http://www.mingw.org/) (GNU-ish Windows layer). Thanks! – Dan Lenski Aug 27 '14 at 17:30
  • 16
    If you're using Python, that'd translate to `sys.stdin = open("/dev/tty", "r")`. Might help someone :) – filaton Feb 09 '16 at 15:13
  • If I dont assign this, where does my standard input go?? – leoOrion Oct 25 '19 at 09:27
  • What can you do if you're on Windows? I am asking because in Windows you don't have the "/dev/tty" file. (asking more precisely for python, but any response I could understand and translate it into python) – Ianau Andrei Jun 17 '20 at 08:42
6

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.

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Frosty
  • 6,213
  • 3
  • 24
  • 20
5
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
sRavioli
  • 27
  • 5
NickUnuchek
  • 11,794
  • 12
  • 98
  • 138
2

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 !"
sRavioli
  • 27
  • 5
Louis BAYLE
  • 81
  • 1
  • 3
2

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);
sRavioli
  • 27
  • 5
dosentmatter
  • 1,494
  • 1
  • 16
  • 23
  • Hi Am a noob in node and git , am planning to write hooks in node js, i have a scenrio where i need to capture user input to run eslint or not,no matter what i do the hook execution happens without a pause for user input , then i read that we have to attach dev/tty i was googling a lot on how to do that , came across your repo, i thought maybe you would have some idea on the same, if this is not the right place to contact, my apologies in advance can you please let me know how to capture user input in git hooks written in node? will your package help me here? – user1651070 May 13 '20 at 13:53
  • I think you are the same person that posted on my [repo](https://github.com/dosentmatter/force-stdin-tty/issues/2). I think you are on windows and `/dev/tty` doesn't exist on windows. You can try using the `CON` device. – dosentmatter May 15 '20 at 17:53
1

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
Community
  • 1
  • 1
perx
  • 11
  • 1