dotancohen
dotancohen

Reputation: 31491

git pre-status or post-status hook

I would like to run a linter on git status, however there seems to be no pre-status nor post-status hook. How could one add a hook to git? The fine docs are suspiciously silent on the matter!

I'm currently wrapping my linter and git status in a Bash script, but I would prefer a solution which supports my muscle-memory-macro git status. I'm running CentOS 7.3 with KDE 4 if it matters.

Upvotes: 15

Views: 2257

Answers (2)

Gabriel Staples
Gabriel Staples

Reputation: 52887

Adding custom git hooks, such as pre-git status and post-git status

Expanding on @Leon's answer, here's a more complete example of how to add custom git hooks, such as pre-git status and post-git status into your ~/.bashrc file.

First off, some notes:

  1. Most git hooks are stored inside the .git/hooks directory for a given repository. If you can use those, do. See here: https://git-scm.com/docs/githooks.
  2. But, it might be useful to place custom git hooks into your ~/.bashrc file (or equivalent) instead under perhaps a few circumstances:
    1. You want to use the same git hook for many different repositories, and you don't want to have to copy it into each one.
    2. You want to easily share the same hooks with different users on different computers. So, you just put this file into a shared tools repository and everyone uses it.
    3. [MOST COMMON] Git doesn't allow you to create the hook you want (ex: a pre-git status hook). So, you manually create it here.
  3. Once you create a custom git wrapper function, as shown below, you can add as many custom hooks as you want to it.
  4. Once you create the git wrapper via the Bash function named git below, you may want to go back to all of your other user-interactive scripts or functions which call git, and change each git call to command git instead.
    1. You'll notice that command git is used in our custom git function below. This is a Bash built-in function which causes the real git command to be called, instead of our custom git function.
    2. In most of your custom scripts calling git, you may not nee your custom hooks to run, so using command git will ensure they don't.

So, here's one way to write a git wrapper to get it done:

First, create a ~/.git_custom_hooks file. Then, source it in your ~/.bashrc file by adding the following to the bottom of your ~/bashrc file:

# Import this ".git_custom_hooks" file, if it exists.
if [ -f "~/.git_custom_hooks" ]; then
    . "~/.git_custom_hooks"
fi

Now, place your custom hooks into the ~/.git_custom_hooks file, like this.

This is from my home/.git_custom_hooks file in my eRCaGuy_dotfiles repo. For the latest version of my custom git hooks, see that file.

# (all `git` cmds)
pre_git_hook()
{
    # echo "=== Running pre_git_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}
post_git_hook()
{
    # echo "=== Running post_git_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}

# status
pre_git_status_hook()
{
    # echo "=== Running pre_git_status_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}
post_git_status_hook()
{
    # echo "=== Running post_git_status_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}

# commit
pre_git_commit_hook()
{
    # echo "=== Running pre_git_commit_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}
post_git_commit_hook()
{
    # echo "=== Running post_git_commit_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}

# push
pre_git_push_hook()
{
    # echo "=== Running pre_git_push_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}
post_git_push_hook()
{
    # echo "=== Running post_git_push_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}

# pull
pre_git_pull_hook()
{
    # echo "=== Running pre_git_pull_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}
post_git_pull_hook()
{
    # echo "=== Running post_git_pull_hook() function. ==="  # debugging
    # do stuff
    true  # do nothing (bash functions can't be empty); delete this line
          # once you add your own code
}


# Outer-most git hook wrapper function
git()
{
    echo "=== Running custom git() wrapper function. ==="  # debugging

    # Note: if no argument is passed in, this just sets cmd to an empty string
    cmd="$1"
    # echo "cmd = $cmd"  # debugging

    # Pre-hooks
    pre_git_hook
    # Note: in Bash if statements, `[[ ]]` is recommended over `[ ]` because
    # `[[ ]]` is faster. See my answer here:
    # https://stackoverflow.com/a/77291070/4561887
    if [[ "$cmd" == "status" ]]; then
        pre_git_status_hook
    elif [[ "$cmd" == "commit" ]]; then
        pre_git_commit_hook
    elif [[ "$cmd" == "push" ]]; then
        pre_git_push_hook
    elif [[ "$cmd" == "pull" ]]; then
        pre_git_pull_hook
    fi

    # Run the actual git command; the `command` built-in is used to force the
    # real `git` command to get called here, to prevent infinite recursion.
    command git "$@"
    return_val="$?"

    # Post-hooks
    if [[ "$cmd" == "status" ]]; then
        post_git_status_hook
    elif [[ "$cmd" == "commit" ]]; then
        post_git_commit_hook
    elif [[ "$cmd" == "push" ]]; then
        post_git_push_hook
    elif [[ "$cmd" == "pull" ]]; then
        post_git_pull_hook
    fi
    post_git_hook

    # Be sure to return the return value of the actual git command, not the
    # return value of this function nor our hooks.
    return "$return_val"
}

Now, either close and re-open each terminal, or re-source your ~/.bashrc file by running . ~/.bashrc in each open terminal.

Example output

With the code exactly as-is above, running any git command will add this to the first line of the output to remind you that it is in effect:

=== Running custom git() wrapper function. ===

Example run and output of git status:

eRCaGuy_dotfiles$ git status
=== Running custom git() wrapper function. ===
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

If you uncomment all of the echo statements in each pre and post hook function, you'll see the following output when you run git status or git push, for instance. Notice all of the lines which begin with === to indicate where the pre and post hooks run:

eRCaGuy_dotfiles$ git status
=== Running custom git() wrapper function. ===
=== Running pre_git_hook() function. ===
=== Running pre_git_status_hook() function. ===
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
    modified:   home/.git_custom_hooks

no changes added to commit (use "git add" and/or "git commit -a")
=== Running post_git_status_hook() function. ===
=== Running post_git_hook() function. ===

And:

eRCaGuy_dotfiles$ git push
=== Running custom git() wrapper function. ===
=== Running pre_git_hook() function. ===
=== Running pre_git_push_hook() function. ===
Everything up-to-date
=== Running post_git_push_hook() function. ===
=== Running post_git_hook() function. ===

I'm going to add a custom pre-git status hook to handle this new problem that cropped up in VSCode in the last couple weeks. Here's my bug report: Saving the settings.json file in VSCode mistakenly wipes and recreates the file with a new inode number, breaking symlinks and hard links to it.

References

  1. @Leon's answer
  2. https://git-scm.com/docs/githooks - official git hook documentation.
  3. My answer showing that [[ ]] is faster than [ ] in Bash if statements`: Are double square brackets [[ ]] preferable over single square brackets [ ] in Bash?

See also

  1. My tutorial/comment here: How to write custom git hooks to automatically copy the changed VSCode settings.json and keybindings.json files whenever they change

Upvotes: 2

Leon
Leon

Reputation: 32494

Git hooks are for operations that (are going to) modify the repository or the working tree. Since git status is a read-only operation there is no hook for it.

I'm currently wrapping my linter and git status in a Bash script, but I would prefer a solution which supports my muscle-memory-macro git status.

You can wrap your git command into the following function that will not require to adjust your muscle memory:

git()
{
    if [[ $# -ge 1 && "$1" == "status" ]]
    then
        echo Your git-status pre-hook should be here
    fi

    command git "$@"
}

Upvotes: 17

Related Questions