reversed(top()) code tags rss about

Repository-specific commands

June 29, 2020
[git] [programming] [terminal] [tools]

Automating typical tasks in your repositories makes life easier by relieving you from running same commands over and over again. However, eventually using the automation itself can start taking more effort than you feel it deserves. At the same time some things are so simple you don't even try to automate them properly or maybe you're not sure they are useful enough to warrant generalization, so they live and evolve in your shell's history. These and some other cases can be handled better with commands/scripts that are local to a specific repository.

You don't really need this in a new project right away, but as more maintenance tasks appear and directory structure becomes more complicated, the need for simplification arises.

Motivating examples

Think of any script in your repository that does some work. Even if it does it perfectly, the script probably has a condition for its execution: it must be run at the root of the repository. Is your shell always at the root of your repository? I doubt that. This forces you to at least go there or type ../ and maybe spend several tries at that just to find out the script doesn't work anyway unless you actually change to the root directory.

You can also think of any group of related files that you need to update simultaneously, or often, or view them in some non-obvious way. Even if you use a fuzzy-finder, you still need to search for them despite knowing target location beforehand.

Another example is a situation when script just can't be generalized up to a point when it works in any environment and you need to tweak it or run some pre- or post-actions. Adding an untracked version will just clutter the repository and won't guarantee presence of the file after git clean.

While trying to address some of the concerns above and to make running scripts more convenient you can try putting them somewhere in the $PATH, however running them in other repositories or at a wrong level of correct repository might have unintended consequences. Moreover, name of these scripts better be long enough to do not hide any other command by accident.

Keeping useful inliners in your shell's history was already mentioned above. This tends to produce multiple versions with different number of preceding ../, versions tailored to different repositories and just newer and older modifications of the same commands. All of this necessitates picking the right version before every use (which is often the most recently used one, except when it's not).

I'm sure there are other examples when you just want something as convenient as a global command which nevertheless is tweaked for specific repository and doesn't exist outside of its bounds.

Solution

Proposed solution is rather simple:

  • a custom directory under .git/ containing repository-specific commands
  • a script that runs those commands from any directory within the repository

Don't underestimate this simple configuration, it's powerful enough to be very useful. Below is the current version of the script in full just to show how straight-forward it is (lots of lines do printing). Git-tracked version is located here.

#!/bin/bash
# Licensed under Apache-2.0

actions_dir="$(git rev-parse --absolute-git-dir 2>/dev/null)/actions/"
if [ $? -ne 0 ]; then
    echo "Git repository isn't found"
    exit 1
fi

export GTDO_WORKTREE_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
if [ $? -ne 0 ]; then
    echo "Can't find worktree"
    exit 2
fi

export GTDO_CWD=$PWD
cd "$GTDO_WORKTREE_ROOT"

actions=
if [ -d "$actions_dir" ]; then
    actions=$(find "$actions_dir" -executable \
                                  -xtype f \
                                  -maxdepth 1 \
                                  -exec basename {} \;)
fi

if [ $# -eq 0 ]; then
    if [ -z "$actions" ]; then
        echo No actions
    else
        echo "Actions of current repository:"
        echo "$actions" | sed 's/^/ * /'
    fi
    exit 0
fi

if [ $# -eq 1 ] && ( [ "$1" = "-h" ] || [ "$1" = "--help" ] ); then
    echo "Usage: $(basename $0) [-h|--help]"
    echo "Usage: $(basename $0) -e|--edit action"
    echo "Usage: $(basename $0) action [args...]"
    echo
    echo "Actions of current repository:"
    echo "$actions" | sed 's/^/ * /'
    exit 0
fi

if [ $# -eq 2 ] && ( [ "$1" = "-e" ] || [ "$1" = "--edit" ] ); then
    if [ "${2:0:1}" = "-" ]; then
        echo 1>&2 "Action name can't start with a dash: $2"
        exit 3
    fi

    actionfile="$actions_dir/$2"
    exec $EDITOR "$actionfile"
fi

if [ "${1:0:1}" = "-" ]; then
    echo 1>&2 "Unrecognized option or invocation form: $1"
    exit 4
fi

action=$1
shift

actionfile="$actions_dir/$action"
if [ ! -f "$actionfile" ]; then
    echo Not an action: "$action"
    exit 5
fi
if [ ! -x "$actionfile" ]; then
    echo Disabled action: "$action"
    exit 6
fi

exec "$actionfile" "$@"

As you can see the name of directory for commands is .git/actions. The script itself is called gt-do, which explains the GTDO_ prefix (gt obviously stands for git).

Apart from running actions from the root of the repository it also sets $GTDO_WORKTREE_ROOT environment variable to have full paths explicitly in actions and $GTDO_CWD to allow referring to original location if necessary.

Usage

gt-do

List commands of current repository.

gt-do -h|--help

List options and commands of current repository.

gt-do -e|--edit action

Open specified command in the editor for editing. If your editor can create non-existing directories and mark scripts executable on saving, this command is also enough to effortlessly create new actions.

gt-do action [args...]

Run specified command named action with the optional list of arguments.

Examples

For vifm I quickly came up with the following commands:

  • check — run tests
  • cov — collect coverage using uncov
  • doc — open documentation files in gVim for editing
  • hi — open files related to syntax highlighting in gVim for editing
  • man — view rendered manual page
  • pub — push code changes to public repositories
  • syn — test changes to Vim's highlighting

In case you wonder if this is really helpful, I assure you that it is. It's much easier to type x check misc instead of something like make -C ../tests misc for quickly running specific tests once. Opening two paths like tests/test-data/syntax-highlight/syntax.vifm and data/vim/syntax/vifm.vim in an already running instance of gVim with a simple x hi is just a breeze.

Possible improvements

Shorter name

gt-do is unique enough to be put somewhere in the $PATH, but it's not convenient to type often.

gdo alias aligns well with the rest of my git-aliases, but it's still longer than it might be.

x is a nice one and can be interpreted as extra commands.

Another good option is a for act. You also don't need to move fingers from home row to type it.

Pick whichever works best for you and doesn't conflict with existing commands.

Completion

Completion of commands won't hurt, but isn't required, because commands can be very short.

Completion of arguments of commands would be nice, but will probably make implementation much more complicated.

Description of commands

Just to remind yourself what it does without looking at the code. The simplest implementation can probably grep script for a one-line description.