Background long-running git hooks
A script that I’ve been using for years stopped working as expected after I upgraded bash
and git
. I use ctags to navigate code in my editor (currently Atom). To automate the generation of the tags
file, I run the ctags
executable from git hooks (post-commit
, post-merge
, and post-checkout
), which fits well with my development workflow.
Some of the projects I work with are quite large, and the ctags
invocation can take longer than 30 seconds. To avoid waiting that long on each commit, I background the invocation. The hook – that has worked for years – looked like this:
#!/usr/bin/env bash
# Regenerate ctags
# Only run one ctags process for this directory at the time.
# Otherwise the ctags file is corrupted
(lockfile .ctags.lock; \
ctags -R --exclude='*.js' --exclude='*.h' --exclude='*.cpp' &> /dev/null ; \
rm -f .ctags.lock) &
The lockfile
usage prevents multiple copies of ctags
running at the same time, which can happen when the hook is invoked often (like when comitting multiple times in quick succession). The (..)
invoke the commands inside on a sub-shell, and the &
at the end tells bash to background the work and continue.
I’ve been using this for years without issue, until I recently upgraded both git
and bash
on my machine. The invocation above continued to generate the tags as expected, but instead of backgrounding the work, the git hook would block until ctags
finished.
I could not find anything related to that in either git
or bash
release notes. StackOverflow provided several tips regarding using nohup
or disown
but using them didn’t help.
Eventually, what did work is redirecting the output of the sub-shell, instead of redirecting the output of ctags
alone:
(lockfile .ctags.lock; \
ctags -R --exclude='*.js' --exclude='*.h' --exclude='*.cpp' ;\
rm -f .ctags.lock) &> /dev/null &
When the sub-shell is instantiated, it’s stdout
and stderr
are connected to the parent process (i.e. the git hook). My best guess is that after the upgrade, the hook invocation now waited until the sub-shell existed because it’s std{out,err}
was connected to the sub-shell’s. With the new invocation, the (..) &> /dev/null
disconnects the output streams for the whole sub-shell from the hook’s output streams, by redirecting it to /dev/null
. The hook’s process can then safely close its own std{out,errr}
and exit.
Find me on Mastodon at @ylansegal@mastodon.sdf.org,
or by email at ylan@{this top domain}
.