10

I'm using one of git's hooks commit-msg to validate a commit message for certain format and contents.

However, whenever a commit message fails the hook, I have sometimes lost a paragraph or more of text from my message.

I've played around with saving it off somewhere, but I'm not sure how to restore it to the user when they attempt to fix the failed commit message, only the last good commit message shows up.

Has anyone else dealt with this before? How did you solve it?

Info: I am using python scripts for my validation.

kelvin
  • 1,421
  • 13
  • 28
Maggie S.
  • 1,076
  • 4
  • 20
  • 30

2 Answers2

20

The commit message is stored in .git/COMMIT_EDITMSG. After a "failed" committing attempt, you could run:

git commit --edit --file=.git/COMMIT_EDITMSG

or shorter, e.g.:

git commit -eF .git/COMMIT_EDITMSG

which will load the bad commit message in your $EDITOR (or the editor you set up in your Git configuration), so that you can try to fix the commit message. You could also set up an alias for the above, with:

git config --global alias.fix-commit 'commit --edit --file=.git/COMMIT_EDITMSG'

and then use git fix-commit instead.

alfunx
  • 3,080
  • 1
  • 12
  • 23
  • Thank you so much! Integrated this alias into my scripts and instructions into the error message. This works like a charm. – Maggie S. Oct 24 '18 at 20:00
  • 2
    @MaggieS. I'd recommend against using the alias itself in the script to keep the script portable and as "abstract" as possible - use the full command instead there. Aliases like this one are rather meant for interactive usage. – alfunx Oct 24 '18 at 20:06
  • Unfortunately, this doesn't work in Debian 10. Running `git commit -eF .git/COMMIT_EDITMSG` opens vim with an empty commit message. But the file is there, so I can at least just copy & pate it into the new (empty) commit message – Michael Altfield Oct 03 '22 at 02:07
2

Background

As stated, when running git commit, git starts your editor pointing to the $GIT_DIR/COMMIT_EDITMSG file. Unless the commit-msg hook in question moves/deletes/damages the file, the message should still be there.

I suppose that reusing the message is not the default behavior because it might interfere with the prepare-commit-msg hook. Ideally, there would be a toggle available to enable reusing by default, in order to avoid data loss. The next-best thing would be to override a git sub-command with a git alias, but unfortunately it is currently not possible and that is unlikely to change. So we are left with creating a custom alias for it. I went with an alias similar to the one in the accepted answer:

git config alias.recommit \
'!git commit -F "$(git rev-parse --git-dir)/COMMIT_EDITMSG" --edit'

Then, when running git recommit, the rejected commit message's content should appear in the editor.

Addition

Note that both aliases would fail for the first commit in the repository, since the COMMIT_EDITMSG file would not have been created yet. To make it also work in that case, it looks a bit more convoluted:

git config alias.recommit \
'!test -f "$(git rev-parse --git-dir)/COMMIT_EDITMSG" &&
git commit -F "$(git rev-parse --git-dir)/COMMIT_EDITMSG" --edit ||
git commit'

Which can be shortened to:

git config alias.recommit \
'!cm="$(git rev-parse --git-dir)/COMMIT_EDITMSG" &&
test -f "$cm" && git commit -F "$cm" --edit || git commit'

Either way, considering the added safety, for interactive usage you could even use one of the aforementioned aliases by default instead of git commit.

You could also make a wrapper for git itself and divert the calls based on the arguments (i.e.: on the sub-command), though that would require ensuring that all subsequent calls to git refer to the original binary, lest they result in infinite recursion:

git () {
    cm="$(git rev-parse --git-dir)/COMMIT_EDITMSG"

    case "$1" in
    commit)
        shift
        test -f "$cm" && command git commit -F "$cm" --edit "$@" ||
        command git commit "$@"
        ;;
    *)
        command git "$@";;
    esac
}

Note that if the above is added to your rc file (e.g.: ~/.bashrc), then every call to git present in it will refer to the wrapper, unless you prepend them with command as well.

Novelty

Finally, I just learned that aliasing to a wrapper file with a different name is an option:

PATH="$HOME/bin:$PATH"
export PATH
alias git='my-git'

So the wrapper (e.g.: ~/bin/my-git) can be much simpler:

#!/bin/sh
cm="$(git rev-parse --git-dir)/COMMIT_EDITMSG"

case "$1" in
commit)
    shift
    test -f "$cm" && git commit -F "$cm" --edit "$@" ||
    git commit "$@"
    ;;
*)
    git "$@";;
esac

And also avoid interference, as aliases are not expanded when used in external scripts.

kelvin
  • 1,421
  • 13
  • 28