~jshholland/inv.alid.pw

e88e66ac850bd2f10e34d928ae4f882863f02c0e — Josh Holland 3 years ago 7a9b9c0
add stgit post
1 files changed, 176 insertions(+), 0 deletions(-)

A posts/stgit-scratch.md
A posts/stgit-scratch.md => posts/stgit-scratch.md +176 -0
@@ 0,0 1,176 @@
---
title: More on git scratch branches: using stgit
published: 2021-04-16
---

I wrote [a short post](/posts/git-scratch-branch/) last year
about a useful workflow for preserving temporary changes in
git by using a scratch branch.  Since then, I've come across
[stgit](https://stacked-git.github.io/), 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](https://ziglang.org) 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](https://github.com/ziglang/zig/wiki/Building-Zig-From-Source#option-a-use-your-system-installed-build-tools)
we'll need to have CMake, GCC or clang and the LLVM development libraries
to build the Zig compiler.  On [NixOS](https://nixos.org/) 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 instructions^[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.].

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`^[an unfortunate naming
overlap between pushing onto a stack and pushing a git repo] to reapply,
and `stg pull` to do a `git pull` and reapply the patch back on top.
The main [stgit documentation](https://stacked-git.github.io/guides/)
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`[^githooks] (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
```

[^githooks]: 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.

Now we can't accidentally^[You can always pass `--no-verify` if you want
to bypass the hook.] shoot ourselves in the foot:
```
$ git push
Unapplied stgit patch found, not pushing
error: failed to push some refs to <remote>
```

Happy stacking!