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 testscov
— collect coverage using uncovdoc
— open documentation files in gVim for editinghi
— open files related to syntax highlighting in gVim for editingman
— view rendered manual pagepub
— push code changes to public repositoriessyn
— 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.