More on git scratch branches: using stgit
16 April 2021I wrote a short post last year about a useful workflow for preserving temporary changes in git by using a scratch branch. Since then, I’ve come across stgit, which can be used in much the same way, but with a few little bells and whistles on top.
Let’s run through a quick example to show how it works. Let’s say I want to play around with the cool new programming language Zig and I want to build the compiler myself. The first step is to grab a source code checkout:
$ git clone https://github.com/ziglang/zig
Cloning into 'zig'...
remote: Enumerating objects: 123298, done.
remote: Counting objects: 100% (938/938), done.
remote: Compressing objects: 100% (445/445), done.
remote: Total 123298 (delta 594), reused 768 (delta 492), pack-reused 122360
Receiving objects: 100% (123298/123298), 111.79 MiB | 6.10 MiB/s, done.
Resolving deltas: 100% (91169/91169), done.
$ cd zig
Now, according to the instructions
we’ll need to have CMake, GCC or clang and the LLVM development
libraries to build the Zig compiler. On NixOS it’s usual to avoid installing
things like this system-wide but instead use a file called
shell.nix
to specify your project-specific dependencies. So
here’s the one ready for Zig (don’t worry if you don’t understand the
Nix code, it’s the stgit workflow I really want to show off):
$ cat > shell.nix << EOF
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [ pkgs.cmake ] ++ (with pkgs.llvmPackages_12; [ clang-unwrapped llvm lld ]);
}
EOF
$ nix-shell
Now we’re in a shell with all the build dependencies, and we can go
ahead with the
mkdir build && cd build && cmake .. && make install
steps from the Zig build instructions1.
But now what do we do with that shell.nix
file?
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
shell.nix
nothing added to commit but untracked files present (use "git add" to track)
We don’t really want to add it to the permanent git history, since
it’s just a temporary file that is only useful to us. But the other
options of just leaving it there untracked or adding it to
.git/info/exclude
are unsatisfactory as well: before I
started using scratch branches and stgit, I often accidentally deleted
my shell.nix
files which were sometimes quite annoying to
have to recreate when I needed to pin specific dependency versions and
so on.
But now we can use stgit to take care of it!
$ stg init # stgit needs to store some metadata about the branch
$ stg new -m 'add nix config'
Now at patch "add-nix-config"
$ stg add shell.nix
$ stg refresh
Now at patch "add-nix-config"
This little dance creates a new commit adding our
shell.nix
managed by stgit. You can stg pop
it
to unapply, stg push
2 to reapply, and
stg pull
to do a git pull
and reapply the
patch back on top. The main stgit documentation is
helpful to explain all the possible operations.
This solves all our problems! We have basically recreated the scratch branch from before, but now we have pre-made tools to apply, un-apply and generally play around with it. The only problem is that it’s really easy to accidentally push your changes back to the upstream branch.
Let’s have another example. Say I’m sold on the stgit workflow, I have a patch at the bottom of my stack adding some local build tweaks and, on top of that, a patch that I’ve just finished working on that I want to push upstream.
$ cd /some/other/project
$ stg series # show all my patches
+ add-nix-config
> fix-that-bug
Now I can use stg commit
to turn my stgit patch into a
real immutable git commit that stgit isn’t going to mess around with any
more:
$ stg commit fix-that-bug
Popped fix-that-bug -- add-nix-config
Pushing patch "fix-that-bug" ... done
Committed 1 patch
Pushing patch "add-nix-config ... done
Now at patch "add-nix-config"
And now what we should do before git push
ing is
stg pop -a
to make sure that we don’t push
add-nix-config
or any other local stgit patches upstream.
Sadly it’s all too easy to forget that, and since stgit updates the
current branch to point at the current patch, just doing
git push
here will include the commit representing the
add-nix-config
patch.
The way to prevent this is through git’s hook system. Save this as
pre-push
3 (make sure it’s executable):
#!/bin/bash
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local sha1> <remote ref> <remote sha1>
remote="$1"
url="$2"
z40=0000000000000000000000000000000000000000
while read local_ref local_sha remote_ref remote_sha
do
if [ "$local_sha" = $z40 ]
then
# Handle delete
:
else
# verify we are on a stgit-controlled branch
git show-ref --verify --quiet "${local_ref}.stgit" || continue
if [ $(stg series --count --applied) -gt 0 ]
then
echo >&2 "Unapplied stgit patch found, not pushing"
exit 1
fi
fi
done
exit 0
Now we can’t accidentally4 shoot ourselves in the foot:
$ git push
Unapplied stgit patch found, not pushing
error: failed to push some refs to <remote>
Happy stacking!
At the time of writing, Zig depends on the newly-released LLVM 12 toolchain, but this hasn’t made it into the nixos-unstable channel yet, so this probably won’t work on your actual NixOS machine.↩︎
an unfortunate naming overlap between pushing onto a stack and pushing a git repo↩︎
A somewhat orthogonal but also useful tip here so that you don’t have to manually add this to every repository is to configure git’s
core.hooksDir
to something like~/.githooks
and put it there.↩︎You can always pass
--no-verify
if you want to bypass the hook.↩︎