Hacking Thy Fearful Symmetry

`git integrate` all humans

February 1st, 2020
git-vaudevillegithook

git integrate all humans

A few months ago I came up with [git-vaudeville][], a git hook management system. Quite a basic affair, but it allows for a few nice tricks; one of them being the creation of custom phase hooks.

In this blog entry, I'll show how it can work using a made-up git command living at the core of my workflows: git integrate.

The motivation: merging good, but integrate

When I code, I have a golden rule: the development branch belongs to me, the main branches belong to the project.

That means that during development I can -- and believe you me, I do -- do all kinds of atrocities. I generate tons of commits with unhelpful "wip" messages. I commit, revert, revert the revert. I break tests. I mix features together. I am, in short, the monster your managed warned you about.

But it also means that once I did what needed to be done, I feel a professional duty -- nay, pride -- to erase, delete, remove, and bleach all of my ignominies. What happens in the kitchen might be a bloody butchery, but what will be placed on your table will be both delicious and Instagram-worthy.

That transition from hacking floor to world presentable follows a pretty consistent list of sanity checks, and grooming actions. The coded needs to be linted according to the project's rules. Commits have to be tidied and aggregated. Swearing and sensitive information has to be filtered out. Etc.

Enter the concept of an integration merge, as a merge targeting an "official" branch for which grooming has to performed in addition of the merging itself.

Years ago I wrote a Perl script implementing such a git integrate commmand (and blogged about it here). It's a script that served me well, but with git-vaudeville, we can break it in smaller bits.

'git integrate' by the way of git-vaudeville

What we want to do is to implement all checks as individual hooks and create a git integrate command that is, at its core, as basic as possible.

Thanks to git picking any git-* executable in the path as a cromulent git subcommand, we can do that by writing this git-integrate shell script:

#!/usr/bin/env fish

set -l branch $argv[1]

set -x GIT_INTEGRATE_ARGS $argv

if test -z $branch
    set branch master
end

git vaudeville run integrate $branch ( git rev-parse HEAD)

set dirty ( git status --porcelain )

if test ! -z $dirty
    echo "oh my, stuff is lying around"
    for line in $dirty
        echo $line
    end
    exit 1
end


git checkout $branch
and git merge --no-ff -

The magic line there is git vaudeville run integrate, which tells git-vaudeville to run all hooks related to the phase integrate. integrate is not a real git phase, but git-vaudeville -- blessed be its simple mind -- doesn't know that and doesn't care.

And thus, because we've created a new pseudo-phase we can use to dump all the tests and actions our hearts desire, the main script is left with a single hard-check, where we verify that the branch is devoid of any littering before the merge. If it is indeed clear, we do a non-fast forward branch and call it a day.

A few hooks

So, what kind of hooks can be implemented? For giggles and inspiration, here's a few I'm using these days.

atop-target

Whenever I can, I rebase my work branch atop the main branch, as it makes for a cleaner history. If I can't, I still try to merge the main branch in, to deal with any conflicts on my own terms.

#!/usr/bin/env fish

set -l target $argv[1]

git branch --contains $target | grep '^\*'

if test $status -ne 0
    echo "not rebased atop $target"
    exit 1
end

echo "rebased atop $target"

lint

A lot of projects have specific linting rules. If I detect one, lint the files I've touched, and commit the cleanup automatically.

#!/usr/bin/env fish

function do_prettier -a target branch
    echo "prettier detected"

    set files ( git diff --name-status $target... \
            | perl -anE'say $F[1] if $F[0] =~ /[MA]/' )


    prettier --write $files
    or exit 1

    git add $files

    git status | grep 'working tree clean'
    or git commit -m "prettier"

end

if test -f prettier.config.js
    do_prettier $argv
else if test -f package.json -a -n (cat package.json | jq .prettier)
    do_prettier $argv
else
    echo "no linter detected"
end

no-console-log

In JS-land, a good debugging session often involves a liberal peppering of console.log. I don't want them to seep in the final product:

#!/usr/bin/env fish

eval contains -- --no-no-console-log $GIT_INTEGRATE_ARGS;
and exit

if test ! -f package.json
    echo "not a javascript repo, skipping"
    exit
end

set -l target $argv[1]
set -l branch $argv[2]

set output (git diff $target...$branch | grep '^\\+.*console.log' )
if test -n "$output"
    echo "console.log detected"
    for line in $output;
        echo $line
    end
    exit 1
end

no-fixup

Any fixup commit should be squished before a merge.

#!/usr/bin/env fish

set -l target $argv[1]

set -l fixups ( git log --oneline $target...| grep 'fixup!')

if test -n "$fixups"
    echo got some 'fixup!' commits to squish
    for line in  $fixups; echo $line; end
    exit 1
end

run-tests

Merging code that causes tests to fail? That's embarrassing.

#!/usr/bin/env fish

if test -f package.json
    npm run test
    exit $status
end

if test -f dist.ini
    dzil test
    exit $status
else if test -d t
    prove -l t test
    exit $status
end

echo "no test framework detected"

Next steps toward an integreater future

For now I'm mostly satisfied with the system as-is. I might create an examples directory as part of git-vaudeville where I'll dump some of my most useful hooks. Something else I have a mind of doing is adding the ability to interactively disabling some hooks for a specific run, for those times when you consciously need to break the rules. But in the meantime... enjoy!

Seen a typo or an error? Submit an edit on GitHub!