Semi-public Git repositories

Okay, maybe I’m just messy, but I’m sure the story below will resonate with a lot of developers.

It often starts with a small file I’m not planning to commit.

I’m trying something, so I write a test. Or I figure out how to deploy the project to one of my servers and put the commands in a script. It assumes my directory layout, my set of installed tools, and probably something I’ve forgotten to document, but it works for me. That’s all I need from it.

Then I add some notes about things to fix or try, along with an idea for a feature that might go nowhere. None of this is ready to publish, but it belongs with the project, so I keep it in the checkout.

After a while, my checkout looks pretty different from what someone gets by cloning the repository. The public code is there, along with all the little things that help me maintain it.

And those little things stick around. They show up every time I run git status, until I’ve learned to ignore them without really looking. I don’t put them in .gitignore, because I don’t necessarily want to publish their names either.

I don’t want someone reading a rough idea in my notes and treating it as a promise. Deployment instructions might also have sensitive stuff in them. And honestly, a useful script can be terrible code, which I don’t want to clean up for public review every time I need to use it.

I could add them to .git/info/exclude, which would make git status quieter. But then I switch machines, clone the project, and get exactly what I asked Git to keep: the public part. All my useful clutter stayed on the other laptop.

I’d like those files to have a history, and I’d like to push them somewhere and get them back with the rest of my work. Git already does all of this for every other file in the project.

I want to share as much of the project as I can. I also want to be able to save unfinished work without having to explain it to everyone who looks at the repository.

So I started exploring an idea: what if I could check these files into the same repository, but only people with a secret key would get them in their checkout?

I already had a lot of what I needed in Turbocrypt, a fast, portable file and directory encryption tool. I use it all the time to back up sensitive files remotely. I also use it to keep some directories on my laptop encrypted, with a password protecting the key. That way, no applications can just read through them unless I step in and unlock them first.

Git encryption has just been implemented, and it changes everything.

Getting private files into Git

Here’s how to add an untracked NOTES.md and an ops/ directory.

From inside the checkout, generate a key outside the repository and tell Turbocrypt to use it:

mkdir -p ~/.config/turbocrypt
turbocrypt keygen --password ~/.config/turbocrypt/maintainer.key
turbocrypt git init --key ~/.config/turbocrypt/maintainer.key

Then add the files that should stay private:

turbocrypt git add NOTES.md ops/
git commit -m "Add maintainer files"
git push

After the push, NOTES.md and ops/deploy.sh stay in place, so the notes can be opened in an editor and the script can run as usual.

What actually went into Git is an encrypted copy of each file, under .enc/. Their names are encrypted, too, along with the list of private paths. Local exclude rules keep the readable copies out of commits, without adding their names to a public ignore file.

So an internal document can sit right beside the public documentation. Editors and other tools just open ordinary files, and there’s no filesystem to mount. That’s the private overlay I wanted.

New helpers added to ops/ are covered by the directory rule, too. Turbocrypt’s Git hooks update the encrypted copies before commits and refresh the readable files after checkouts, merges, and rebases.

If only private files have changed, run this to update and stage the encrypted copies before committing:

turbocrypt git encrypt
git commit -m "Update maintainer files"
git push

turbocrypt git status shows which private files have changed.

On another machine, clone the repository like usual, copy the key over separately, and run this from the checkout:

turbocrypt git unlock --key ~/.config/turbocrypt/maintainer.key

The notes and the deployment script show up at their original paths, ready to use. Their encrypted versions came through the same remote and the same history as the public code.

Someone cloning without the key still gets the public project and can build it or contribute without installing Turbocrypt. They get the encrypted store, too, but the private files don’t show up in their working directory.

Making room for another maintainer

My deployment script is probably useless to another maintainer who’s got a different setup. They can keep their own scripts in the same repository with a separate key.

Maintainers don’t have to agree on what belongs in a shared private directory, because each key gets its own area under .enc/.

Turbocrypt restores the files belonging to the checkout’s key and leaves the others encrypted, including their lists of private paths.

So several maintainers can push private files to the same repository, then unlock their own notes and tools in a fresh clone.

They start with turbocrypt git init --key ... when their key is new to the repository. The next time they clone it, they use turbocrypt git unlock --key ... to get their files back. Right now, each checkout uses one key.

Sharing a key also lets a team work on a feature before it’s ready to publish. If the code depends on an internal API that hasn’t reached production yet, the team can commit the prototype and its documentation while waiting for the rollout.

When it’s ready, turbocrypt git rm <path> takes a file out of private management. The readable copy stays on disk, ready for an ordinary git add and a public commit.

I’ve written before about what happens when people start picking apart unfinished experiments. You push a branch because it’s convenient, and suddenly people think you’ve announced the future of the project. I’d like to try an idea with another maintainer and drop it if it doesn’t work out. If it does, we can publish it when we’ve got something worth showing.

How this compares to git-crypt

Git-crypt implements a similar idea.

But I also wanted to hide the names.

A file called plans-to-replace-the-database.md already tells people a lot, even if they can’t read a word of it. And I wanted my private files to show up alongside the public files, with Git tracking their encrypted copies separately.

  Turbocrypt git-crypt
Names HCTR2 encrypts file and directory names. The private path list is encrypted, too. Names stay visible. Rules usually go in a committed .gitattributes.
Everyday use Hooks sync working files with encrypted copies in .enc/. Git filters encrypt on the way into Git and decrypt on checkout.
Separate keys Each checkout restores its key’s files and skips the others. Named keys let collaborators unlock different sets of files.

For convenience, hooks need to run without a password prompt every time, so Git mode keeps an unlocked copy of the key in .git/turbocrypt/key, with restricted permissions. The password still protects the original key file, but programs running under the same user account can read the unlocked copy and the working files.

I think this is cool and useful, and it can probably be useful to you, too. So, give it a try!