Home

Automatic gtags integration for Emacs using Git

(Updated: )

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.

🔗Git

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.

The git grep command is used instead of git ls-files primarily to avoid matching submodules, since including directories causes warnings when running gtags.

Next, add hooks that wrap this script. The first three are post-commit, post-merge, and post-checkout and should contain the following:

#!/bin/sh
.git/hooks/gtags >/dev/null 2>&1 &

Lastly, add one for post-rewrite:

#!/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.

🔗Emacs

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, GTAGSROOT and GTAGSDBPATH.

(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 advice.

🔗Counsel (Ivy)

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)

🔗Helm

For helm-gtags:

(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.