Skip to content

MicahElliott/captain

Repository files navigation

Captain

Captain is a simple, convenient, transparent opt-in approach to client- and CI-side git-hook management, with just a single, tiny, dependency-free shell script to download. Suited for sharing across a team, extensible for individuals. Supports all common git hooks (and probably more)! Works with Linux, MacOS, BSDs, probably WSL. Language-agnositic — no npm, ruby, yaml or anything to wrestle with.

⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣤⣤⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⡿⢿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀
⠀⣠⣤⣶⣶⣿⣿⣿⣿⣯⠀⠀⣽⣿⣿⣿⣿⣷⣶⣤⣄⠀
⢸⣿⣿⣿⣿⣿⣿⣿⣿⡅⠉⠉⢨⣿⣿⣿⣿⣿⣿⣿⣿⡇
⠈⠻⣿⣿⣿⣿⣿⣿⣿⣥⣴⣦⣬⣿⣿⣿⣿⣿⣿⣿⠟⠁
⠀⠀⢸⣿⡿⠿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⠿⢿⣿⡇⠀⠀
⠀⣠⣾⣿⠂⠀⠀⣤⣄⠀⠀⢰⣿⣿⣿⣿⡆⠐⣿⣷⣄⠀
⠀⣿⣿⡀⠀⠀⠈⠿⠟⠀⠀⠈⠻⣿⣿⡿⠃⠀⢀⣿⣿⠀
⠀⠘⠻⢿⣷⡀⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⢀⣾⡿⠟⠃⠀
⠀⠀⠀⠸⣿⣿⣷⣦⣾⣿⣿⣿⣿⣦⣴⣾⣿⣿⡇⠀⠀⠀  Aye, I'll be sinkin me hooks inta yer gits!
⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠛⠛⠛⠋⠉⠀⠀⠀⠀⠀

One-minute E-Z Quick-Start Guide (very easy, point your team here)

SITUATION: Captain was already set up in a repo you use, and you want to start enabling its checks (AKA triggers). (Or you're a curmudgeon: You won't be impacted if you do nothing; then capt will not be invoked — but you will miss out on the fun!)

# Install the capt command (a small zsh script)
cd ~/src # or somewhere like that where you keep clones
git clone https://github.com/MicahElliott/captain  # to get all tooling
print 'path+=~/src/captain/bin' >> ~/.zshrc  # or something/somewhere like that
# OR, put that ^^^ into a .envrc file and use https://github.com/direnv/direnv for your proj
# OR, for just the capt script (sufficient for some projects that don't need extra goodies):
# cd /somewhere/on/your/PATH
# wget https://raw.githubusercontent.com/MicahElliott/captain/main/capt && chmod +x capt

# Point git to the new hooks
cd your-project-root # like you always do
git config core.hooksPath .capt/hooks  # THIS IS THE BIGGIE!!
# Make some project file changes, and
git commit # etc, just like always, nothing you do changed except NOW CLEAN CODE
# Captain at yer service! ...

(Note to MacOS users: If you use a git client/IDE that is not started from a terminal, you'll need to ensure your PATH is set to include /path/to/captain by editing /etc/paths, as per this.)

If there are any "triggers" (linters, formatters, informers, etc) being invoked that you don't have installed yet, Captain should kindly let you know more details.

OR, if you're looking to be the one to introduce Captain git-hook management to a project, read on....

Table of Contents

Do I need a hook manager?

Short answer: Yes!!

Without a hook manager, it's challenging to have a single set of checks (linters, formatters, cleaners, etc) that all developers agree on. Also, having multiple objectives/tasks in a single hook file gets slow and ugly. Managers give you organization, concurrency, shortcut facilities, clarity, consistency, and much more. Over time, you come up with more ideas for things that can run automatically as checks, and eventually your standard unmanaged hook files get messy.

Take a single pre-commit git-hook for example. You’ll want (for all devs on each commit):

  • a variety of tools (linters, formatters, scanners, testers, etc) to be active
  • those tools to run on various OSs despite inconsistent deps installed
  • selectivity of which files are run against (not the whole code base!)
  • timing info of each tool (with slow ones identified)
  • consistent and clear output, drawing attention only to problems
  • ignorability of some things on some systems

You can’t have all that without a manager — you end up cooking it yourself, half-baked. And Yes, you can simply set that all up in your CI (and you should), but you don’t want your devs waiting 15 minutes to see if their commit passed. Instead, you want them to wait a few seconds for all that to run locally, in parallel.

Captain’s key features

Specifically, here are some of Captain's features you don't want to have to invent, write, and/or wrap around every tool you run:

  • Checking for existence and guiding installation of tool being run
  • Timing info of each hook run
  • Clear output made consistent for each tool
  • Files changed precise detection and control
  • File filtering by file type/extension
  • Single file organization of all hook/script specs for whole team to control/use
  • User-Local scripts support for individual developer use
  • Built-In one-word triggers (linters, etc) with pre-defined filters
  • Add-On linters provided for optional use
  • OS-agnostic commands
  • Parallel execution control of each tool
  • Debugging aids for writing your own new scripts
  • Custom scripts location for collecting in your repo

You can think of Captain as like moving your fancy CI setup into everyone’s local control. The output is reminiscent of Github Actions, but way easier to set up, runs automatically whenever you use git, and delivers the red and green a kajillion times faster.

Why Captain instead of another hook manager?

Compared to Lefthook, Husky, and Overcommit, Captain is:

  • Tiny, transparent, no deps: read and understand the whole code base (one small Zsh file) in minutes
  • Simple: workflow is just calling commands or the scripts you already have
  • Client-compatible: other managers don't play nice with some git clients (eg, magit)
  • Super fast: sensitivity-colored timing details apparent everywhere
  • Any terminal (xterm etc): uses unicode indicators instead of emojis, tuned for 80-char display width
  • Basic: config is just a .capt/share.sh control file with shell arrays of scripts for each hook (no yaml etc)
  • Clean, clear, and concise: your standard git-hooks become one-line calls to capt (not cluttered messes)
  • Fun: get ideas for new triggers and be entertained by the Captain!
  • All documentation right here: this readme is all you need
  • Language/tool agnostic: don't need npm, yarn/2, gem, pip, etc; works with any code base
  • Hands-off: Captain doesn't try to install things for you (see External Tools Installation below)
  • Extensible, custom for each dev: run your own triggers in addition to standards

Captain also has most of the features of other managers:

  • Shareable: your whole team has a set of common hooks/triggers
  • Batteries: vars for which files changed, multi-OS functions, extra built-in hooks
  • Customizeable: run triggers in parallel, with verbosity, etc; run from personal dirs or team's

Installation

Sneaking it in

It’s worth noting that no one needs to know you’ve enlisted the Captain. You can do all the following and put capt to work for just yourself to start out with. You’ll commit a .capt/ dir with some innocuous tiny files and point your own git config to use the Captain’s hooks instead of the pedestrian hooks you may have in .git/hooks.

Get it all working

Each developer of your code base is encouraged to install Captain (point them to the One-minute guide above), so violations can be caught before code changes go to CI.

  1. git clone https://github.com/MicahElliott/captain
  2. Try it out! Add your project/company to the Featured Projects section and run capt.
  3. Put the capt script on your path
  4. cd your-project
  5. Run the for-loop below to create any git-hooks you want
  6. Create a .capt/share.sh control file (or copy the one below)
  7. [optional] Create a .capt/local.sh control file for your personal additional triggers

The capt command is invoked with a single argument: the git-hook to run; e.g., as capt pre-commit; that will run all the pre-commit triggers. You can optionally run capt directly to see/debug output, and then have all of git-hooks call it.

Setup and Configuration

Say you want to enable some git-hooks. Here's how you would create the them, just like you may have done in the past with git. This step can be done by each developer upon cloning the project repo:

## If you want to commit the hooks to repo, and everyone sets hooksPath
hookdir=.capt/hooks
mkdir -p $hookdir
git config core.hooksPath $hookdir
## OR, use git's default location, not in repo; everyone has to do the hook creation
# hookdir=.git/hooks
## Create the standard executable git hook files
for hookfile in pre-commit prepare-commit-msg commit-msg post-commit post-checkout pre-push post-rewrite; do
    echo 'capt $(basename $0) $@' > $hookdir/$hookfile
    chmod +x $hookdir/$hookfile
done

Now your $hookdir looks like this:

.capt/hooks/  # or .git/hooks/
├── commit-message
├── post-checkout
├── post-commit
├── post-rewrite
└── pre-commit
└── pre-commit-msg
└── pre-push

And each of those just contains a one-line invocation of the capt command. That enables git to do its default thing: next time you (or anyone) does a git commit, git will fire its default pre-commit script (you just created that) which calls capt with git's args. Then capt does its job of finding the .capt/share.sh control file (and optionally .capt/local.sh) that you created.

Now you can put all those trivial one-liner git-hooks into your project's repo:

echo '.capt/local.sh' >>.gitignore # discussed below
git add .capt
git commit -m 'Add capt-driven git hooks etc (PSA: install capt and set hooksPath)'

That saves all your fellow developers from having to do anything but set: git config core.hooksPath $hookdir, and you can simply point to the One-minute instructions above.

Note on External Tools Installation

It is outside Captain's scope to install all your team's trigger tools on every dev's machine. However, this repo provides an example script that should demonstrate common practice for teams, to get everyone on the same page. Basically, a project should have a script (or at least a doc) for getting all the tooling installed. It might be just a bunch of dnf/apt-get/pacman/brew commands, or it could even be an ansible file.

Control File Spec

Now onto the simple .capt/share.sh control file at the root of your repo (which should also be committed), containing a set of "triggers" for each hook. (Note that git-hooks purposes are written about here.)

Trigger Spec

There is a tiny DSL that is used for each "trigger" in a control file.

    'lint(clj|cljs):   clj-kondo $CAPT_CHANGES &'      # linting of files
     ^^^^ ^^^^^^^^     ^^^^^^^^^^^^^^^^^^^^^^^ ^       ^^^^^^^^^^^^^^^^^^
     NAME  FILTERS             COMMAND     CONCURRENCY    COMMENT

Note that this syntax looks almost exactly like the standard git conventional commits DSL.

Example Team Control File

### Captain git-hook manager control file

# params: NONE
# Common hook with several triggers for linting, formatting, and running tests
pre_commit=(
    'lint:             clj-kondo $CAPT_CHANGES &' # linting of files being committed
    'format(clj|cljc): cljfmt &'                  # reformat or check for poor formatting
    'fixmes:           git-confirm.sh'            # look for/prompt on FIXMEs etc
    markdownlint                                  # built-in config with implicit filter
    'test-suite:       run-minimal-test-suite $CAPT_CHANGES'
)
# params: tmp-message-file-path, commit-type, sha
# Build a commit message based on branch name standardized format.
prepare_commit_msg=(
    # you/TEAM-123_FIX_lang_undo-the-widget-munging => fix(lang): Undo the widget munging #123
    branch2message
)
# params: tmp-message-file-path
# Validate your project state or commit message before allowing a commit to go through
commit_msg=(
    'commitlint: msglint $GITARG1'  # ensure log message meets standards
)
# params: NONE
# Examples: moving in large binary files that you don’t want source
# controlled, auto-generating documentation, etc
# General informative notices, no parameters
post_commit=(
    "stimulate: play-post-commit-sound.sh"           # happy music on successful commit
    "colorize:  commit-colors $(git rev-parse HEAD)" # more confirmation rewards
)
# params: command that triggered rewrite, plus stdin for list of rewrites
# Run by commands that replace commits, (amend/rebase); same uses as post-checkout, post-merge
post_rewrite=(
)

# params: NONE
# Set up your working directory properly for your project environment
post_checkout=(
    "mig-alert(sql): alert-migrations-pending.zsh" # inform that action is needed
)
# Use to validate a set of ref updates before a push occurs
pre_push=(
)
# Not a git hook!
clean_up=(
    'tmpclean: rm **/*.tmp'
    'artclean: rm tmp/*artifact*'
)


# IDEA: maybe let user specify install recipes
installables=(
    'splint(linux)' 'bbin splint'
    'splint(macos)' 'brew install splint'
)

Some things to notice in that file:

  • All the triggers are short and live in a single place
  • Each "hook section" is just a shell array named for git's conventions (but underscores)
  • Some triggers are a line with a somename: "name" prefix, then the eval'd command
  • After a name is an optional "filter": cljfmt will only look at .clj and .cljc files
  • The lint and format are run in parallel by being backgrounded (&)
  • You generally should use single-quote commands, even with env vars
  • The $CAPT_CHANGES is the convenient list of files that are part of the commit
  • The $GITARG1 is the first available param passed from git to a hook script
  • The test-suite is a local script (in .capt/scripts/) not on path; Captain figures that out
  • .capt/share.sh gets put into git at your project-root and is used by all devs on the project
  • The last clean_up hook isn't a git hook, but you can run it directly with capt cli

TODO will likely add these soon

  • disabled (#, commented out from start)
  • fail-ok mode (leading -)
  • description (trailing :: or # some text explaining hook)

User-local additional hooks

Suppose you have even higher personal standards than the rest of your team. E.g., you have OCD about line length. You can ensure that all of your commits conform by creating another local-only .capt/local.sh control file:

pre_commit=( 'line-length-pedant: check-line-length' ... other-custom-triggers... )

Then you should add .capt/local.sh to your .gitignore file.

Settings

You can fine-tune Captain’s behavior with several environment variables.

  • CAPT_VERBOSE :: Set to 1 to enable debug mode
  • CAPT_DISABLE :: Set to 1 to bypass captain doing anything
  • CAPT_MAIN_BRANCH :: Useful for running in CI since default will be feature branch
  • CAPT_FILE :: Team-shared control file containing global hooks/triggers
  • CAPT_LOCALFILE :: User-local personal control file each dev may have (not in git control)
  • CAPT_HOOKSDIR :: Defaults to .capt/hooks, for pointing git to
  • CAPT_SCRIPTSDIR :: Defaults to .capt/scripts, for storing team-shared triggers

There are also arrrgs you can utilize from your control files:

  • CAPT_FILES_CHANGED :: Array of files changed on branch
  • GIT_ARG1 :: First arg git sends to hook
  • GIT_ARG2 :: Second arg git sends to hook
  • GIT_ARG3 :: Third arg git sends to hook

Sample Run

Rather than a live demo, here's an example of a pre-commit run (doesn't correspond to triggers shown above). This shows a couple of team-shared checks (clj-kondo and fixmes), and then after the parrot, a single user-local something trigger:

(◕‿-) CAPTAIN IS OVERHAULIN. NO QUARTER!
       _________
      |-_ .-. _-|
      |  (*^*)  |
      |_-"|H|"-_|

(◕‿-) Loadin the gunwales: /home/mde/work/fooproj/.capt/share.sh

(◕‿-) === PRE-COMMIT ===

(◕‿-) Discoverin yer MAIN branch remotely...
(◕‿-) Main branch bein compared against: master
(◕‿-) Files changed in yer stage (10):
(◕‿-) - base/src/main/clojure/foo/core.clj
(◕‿-) - resources/sql/some-queriees.sql
(◕‿-) - ...
(◕‿-) Execution awaits!
(◕‿-) - clj-kondo
(◕‿-) - fixmes

(◕‿-) ??? CLJ-KONDO ???
(◕‿-) Files under siege: 10
maybe some output from clj-kondo, but assume all is well
(◕‿-) Ahoy! Aimin our built-in cannon with files: $CAPT_FILES_CHANGED
(◕‿-) ✓✓✓ SURVIVAL! (time: 2ms) ✓✓✓

(◕‿-) ??? FIXMES ???
(◕‿-) Ye took care of file selection yerself, or no files needin fer sayin.
(◕‿-) Ahoy! Aimin yer cannon: fixmes: git-confirm.sh
Git-Confirm: hooks.confirm.match not set, defaulting to 'TODO'
Add matches with `git config --add hooks.confirm.match "string-to-match"`
(◕‿-) ✓✓✓ SURVIVAL! (time: 12ms) ✓✓✓

(◕‿-) Ye survived the barrage. Must have been a fluke.

         \
         (o>
      ___(()___
         ||

(◕‿-) Next on the plank: user-local hook scripts
(◕‿-) Loadin the gunwales: /home/mde/work/cc/.capt/local.sh

(◕‿-) Execution awaits!
(◕‿-) - something

(◕‿-) ??? SOMETHING ???
(◕‿-) Ye took care of file selection yerself, or no files needin fer sayin.
(◕‿-) Ahoy! Aimin yer cannon: something: sayhi.zsh
some output from sayhi
(◕‿-) ✓✓✓ SURVIVAL! (time: 3ms) ✓✓✓

(◕‿-) Ye survived the barrage. Must have been a fluke.

(◕‿-) Show a leg!
hint: Waiting for your editor to close the file...

Migrating your existing git-hooks

You can either take the plunge and clean up, separate, and move your existing hooks into .capt/share.sh, OR keep existing git-hooks intact, and just add this to the bottom of each you care about:

# exiting pre-commit: .git/hooks/pre-commit
bunch of ad hoc jank
...

which capt >/dev/null && capt $(basename $0) $@

Use Captain directly outside of git

You can run any individual hook with capt directly. This can sometimes be useful for debugging; or convenience, in case you want to use Captain as something of a task collector.

To run a hook:

capt pre-commit # git standard
## OR
capt my-weird-collection

How to bring Captain to your team

Try using a new hook locally on your own for a while. Once you're confident it does its thing well, confirm with the team that you're moving it into the shared .capt/scripts/ dir. If this is a script that will block their commit or build, you want to make sure everyone is aware and knows how to comply with it.

Treasure trove of hooks

There is a wealth of git-hooks in the wild, and of course you can come up with your own. Here is a list of themes to start with:

  • linting: code, docs (safety etc violations, fixmes)
  • formatting: code, commit messages (style, indentation, whitespace, line length)
  • alerting: migrations to be run
  • deprecations: insecure or outdated deps
  • audio effects: good or bad things completed

Here is a list of available hooks in Overcommit for inspiration.

And here is a list of common hooks that any project may want to leverage, regardless of language:

Tooling Tips

Browser notifications

Try out notifier for github to get real-time desktop pop-up notifications about your builds completing.

Magit

Use page-break-lines for nice, clear sectioning, turning page-breaks (^L) into colored lines. Those lines can also be navigated with C-x [ (prev) and C-x ] (next). Add magit-process-mode to page-break-lines-modes to make them visible in magit-process.

Set environment variables that capt will read with M-x setenv. Eg, if you want to enable verbose logging mode, set CAPT_VEBOSE to 1 with that.

Running Hook Scripts in CI

So you have all these great hook scripts in .capt/scripts now, but do you also want to run them as part of your Continuous Integration? Well, it can be done! Captain was originally conceived more as a dev-side tool, but you don't want to reinvent a bunch of checks to run in CI too, so here is a recipe for setting up the scripts in CI (specifically Github Actions, but should work with other CIs too):

  1. install zsh (github sorely lacks it) and capt during the CI run, OR
  2. set the CAPT_MAIN_BRANCH when invoking capt

My experience is that doing (1) may add ~12 seconds to your CI run (if you don’t do some package caching, in which case it should be ~1 second). If that's fine, it could be nice to have the consistency with what you run locally. Your invocations of those scripts can look like:

    # fire all the pre-commit scripts (yes, it's already committed)
    run: CAPT_MAIN_BRANCH=origin/main capt integration

See the example workflows capt.yml file.

If you care about optimizing the amount of work the scripts do, you may need to have them be smart about file filtering (file-name extensions, which files changed in the commit, etc). In practice, the filtering often isn't too important with the CI runs, since there you might want to go ahead and run all your tests and analyzers, etc, over your whole code base anyway.

Troubleshooting

If for any reason you need to bypass Captain, set this: export CAPT_DISABLE=1

Featured Projects Using Captain

  \\
   (o>
   //\
___V_/_____
   ||
   ||

License

Copyright © Micah Elliott.

Distributed under the Eclipse Public License v2.0. See LICENSE.

About

A simpler approach to git-hook management

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages