It’s fairly common to see automatic ctags hooks setup for Vim, but until recently I haven’t been able to get it working in Emacs. Here’s how to do it.
First, create a directory to contain the git hooks to be added in all new repos.
git config --global init.templatedir '~/.git_template' mkdir -p ~/.git_template/hooks
Next, add the main script. Place this in
~/.git_template/hooks/gtags and mark as executable:
#!/bin/sh set -o errexit -o nounset PATH="/usr/local/bin:$PATH" main() ( root_dir="$(git rev-parse --show-toplevel)" git_dir="$(git rev-parse --git-dir)" cd "$root_dir" trap 'rm -f GPATH GRTAGS GTAGS gtags.files' EXIT # Match non-empty text files only (no submodules). git grep -I --cached --files-with-matches "" > gtags.files gtags --gtagslabel=pygments rm gtags.files mv GPATH GRTAGS GTAGS "$git_dir/" echo "gtags index created at $git_dir/GTAGS" ) main
Note that the generated tags file is in placed in the
.git directory, to avoid cluttering up the directory tree and having to add another entry in
.gitignore. This is the key feature for me — it makes it feel truly automatic and seamless.
git grep command is used instead of
git ls-files primarily to avoid matching submodules, since including directories causes warnings when running
Next, add hooks that wrap this script. The first three are
post-checkout and should contain the following:
#!/bin/sh .git/hooks/gtags >/dev/null 2>&1 &
Lastly, add one for
#!/bin/sh case "$1" in rebase) exec .git/hooks/post-merge ;; esac
Once finished, use
git init and
git gtags in existing repositories to copy the hooks in and generate tags. New repositories will do this automatically.
To get this working in Emacs depends on which gtags package you have installed. Unfortunately,
global does not have an option to directly change where the tags file is read from, and neither do any of the gtags packages I’ve seen. However, it is possible to set two environment variables to attain this functionality instead,
(defun gtags-env-patch (orig &rest args) (if-let* ((project-root (file-truename (locate-dominating-file "." ".git"))) (git-dir (expand-file-name ".git" project-root)) (process-environment (append (list (format "GTAGSROOT=%s" project-root) (format "GTAGSDBPATH=%s" git-dir)) process-environment))) (apply orig args) (apply orig args)))
Then, you can wrap the appropriate functions using
For counsel-gtags (i.e., ivy completion):
(advice-add #'counsel-gtags-find-reference :around #'gtags-env-patch) (advice-add #'counsel-gtags-find-symbol :around #'gtags-env-patch) (advice-add #'counsel-gtags-find-definition :around #'gtags-env-patch) (advice-add #'counsel-gtags-dwim :around #'gtags-env-patch)
(advice-add #'helm-gtags-find-tag :around #gtags-env-patch) (advice-add #'helm-gtags-dwim :around #'gtags-env-patch) (advice-add #'helm-gtags-find-tag-other-window #'gtags-env-patch)
That’s it. Now any new repositories will be automatically indexed whenever they are checked out, committed, or rebased, and the tags file will be found seamlessly without any user input.
A working example can be found here in my setup repo.
If you have a better method or suggested fix, please shoot me an email or comment on the Reddit post.