fun experiments and hardware

Developing for CircuitPython with git-worktree

For the MIDI keyboard, I’ve started using CircuitPython for the firmware. I’m really liking it so far, it’s very easy to write and the repl and near-instant refresh are really valuable.

For those that aren’t familiar, CircuitPython is a port of python for microcontrollers. You can download and install the CircuitPython firmware onto an MCU and then add your application in Python code on top of that.

On recent microcontrollers that have the required USB hardware, the board shows up as a composite USB device with a Serial port (with the Python REPL) and a Storage device (with the Python code and resources).

To install application code, you just write the files to the latter. Whenever a change is detected, CircuitPython restarts the application code.

I think this is a really great approach, especially for beginners: You can get started programming without installing any specific tools at all - Create a file, open it in any text editor you want and see results instantly.

It also means that wherever you go, you always have the last source right with you. It was great to be able to pick up where I left off with someone else’s office computer while on a trip without installing anything for example.

the question

The one thing I was left wondering about though was how to integrate this with source control. There’s two obvious ways:

  1. Work in a repo on your disk.

    Then you’d have to copy all the code over whenever you want to test it. This could be automated with tools like entr and rsync.

    The downside of this approach is that you lose a lot of the benefits mentioned above. If you’re on the go and make changes in the “live” code, you’ll have to copy it over the other direction and check it in, or you risk losing them for example.

    It’s also more tools to install and more command lines to remember.

  2. Work in a repo on the board’s flash.

    Technically this might work, but it’s probably a bad idea for multiple reasons:

    • the repo history is wasting the limited amount of flash space
    • git commands will trigger circuitpython reloads
    • risk of losing the whole repo if hardware fails
    • the flash has a limited amount of write-cycles and the repo will cause

enter git-worktree

git worktree is a git command that allows you to create additional “worktrees” - directories that contain a view of the repository - that can move switch between branches, stage and commit changes, etc., independently of the “main” worktree (the orignal repository).

Importantly, an external worktree doesn’t have a .git directory containing the history of the project, it just has a file linking it to the actual repo. This solves all the issues of approach 2 mentioned above.

making a worktree on an external drive

To create an external worktree, you need a git repository with at least one commit. If you haven’t created a repository yet, you’ll need to either initialize your repository by copying the code from your board or adding a placeholder file. Once you have created the worktree, you can amend/rebase this first commit or start over with an “orphan” branch if you like.

To create the worktree, we’ll use git worktree add, but there’s two small catches in our case. The first one is mentioned in the manpage:

If a worktree is on a portable device or network share which is not always mounted, lock it to prevent its administrative files from being pruned automatically. This also prevents it from being moved or deleted.

By passing --lock when creating the worktree you can lock it immediately. The other issue that git won’t let you create a worktree if the chosen directory isn’t completely empty.

You could delete everything on your board, but you might have some files there that you want to keep or that the system creates for you (e.g. Windows' System Volume Information).

An alternative is to first create the worktree in a regular directory, then swap that out for the board’s drive and move the .git file that was created. It’s important to create the temporary worktree at the same path that the board will be mounted at so that git can find it.

Here’s what I did. I’m using sudo and chown because I want to have the worktree be in /run/media (which is root-owned) where my board will be automounted by udisks when I connect it:

$ # with the board detached, create an empty placeholder
$ sudo mkdir /run/media/s-ol/hex33board
$ sudo chown s-ol:s-ol /run/media/s-ol/hex33board

$ # this adds the worktree and creates /run/media/s-ol/hex33board/.git 
$ git worktree add --lock -b live /run/media/s-ol/hex33board

$ mv /run/media/s-ol/hex33board/.git /tmp/dotgit
$ rm -rf /run/media/s-ol/hex33board
$ # (plug in the board now)
$ mv /tmp/dotgit /run/media/s-ol/hex33board/.git

Now you can start working in the board directory and use git commands as usual to stage and commit changes (e.g. to the live branch in this case).