Have you ever wanted to make local changes to a repository for your own benefit, but don’t want to send them remotely? Maybe you’ve once wanted to:

  • Work around scripts insisting that you have a tool like yarn installed globally, but you correctly recognize that as being stupid and need to wrap it?
  • Add some Dockerfiles for more productive development on your end, but only want to keep them to your machine?
  • Append some extra helpful commands to a package.json to quickly rebuild or test things?
  • Change some invocation flags (e.g. configs, heap size, local web server ports) to make stuff work better on your own system?
  • Patch out the execution of long-running tests locally, and leave them to the CI instead?
  • Nix-ify the development environment so that installing the build tools won’t conflict with anything else on your system?
  • Develop on OSX but the rest of the team is on Linux so your build scripts need adjusting?
  • Make literally any kind of suit-tailored change for your own setup?

You’ve probably had to do such things before and ended up with a small mess of untracked files, changes that you need to be ‘careful not to accidentally stage’, and loose-leaf changes scattered in places.

We can do much better. You can productively accomplish all this and more with a pattern I like to use and abuse: the dev branch.

Overview

The idea is simple. We’ll make a new branch called dev, configure git to not push commits from it anywhere, put all our custom tailorings in it, and then learn how to use it as our new base of operations when writing pull requests.

Oh, yeah, if you’re not using git then I don’t know what to tell you. Stop reading here, I guess.

Make a new branch off of main, and call it dev.

$ git checkout main
$ git checkout -b dev

This branch will be what we use all the time now. We’re almost never going to spend time on main. The dev branch is where we hang out now. Especially on the weekends, or if it’s raining.

Set up pre-commit githooks

We’re going to be adding our bespoke, tailor-made commits to this branch and we don’t want to share them with anybody else. We’ll use git hooks to prevent accidental sharing.

Inside the .git/hooks/ folder, there should be a pre-push.sample example of a pre-push hook. If you don’t have it, you can find it on github here.

Copy it without the .sample:

$ cp .git/hooks/pre-push.sample .git/hooks/pre-push

The example shows how to prevent pushing of commits that start with “WIP”. We’re going to edit it to prevent pushing commits that contain “nocommit” anywhere in the message. Apply this patch:

@@ -19,6 +19,9 @@
 # This sample shows how to prevent push of commits where the log message starts
 # with "WIP" (work in progress).
 
+
+echo "Checking for 'nocommit' commits"
+
 remote="$1"
 url="$2"
 
@@ -40,11 +43,11 @@
                        range="$remote_oid..$local_oid"
                fi
 
-               # Check for WIP commit
-               commit=$(git rev-list -n 1 --grep '^WIP' "$range")
+               # Check for NOCOMMIT commits
+               commit=$(git rev-list -n 1 --grep '.*nocommit.*' "$range")
                if test -n "$commit"
                then
-                       echo >&2 "Found WIP commit in $local_ref, not pushing"
+                       echo >&2 "Found NOCOMMIT commit in $local_ref, not pushing"
                        exit 1
                fi
        fi

By putting it into a file and using it like this:

$ patch .git/hooks/pre-push < patchfile

Test the push hook

We’re now going to make an ‘empty’ commit, and try to push it. It should fail. If it succeeds – no harm done, nobody is going to really notice anything.

$ git commit -m "sentinel - nocommit - sentinel" --allow-empty
$ git push origin dev

Git should refuse to push this. If so, then it worked. If it did push just fine, delete the remote branch, debug it and try again.

Be very careful about making sure the hook is present. It is not stored in git itself, but only on your local filesystem. Take care if you move your dev branch to another machine and push.

Make all the tailorings you want

This “sentinel commit” will refuse to be pushed. That also means that any commits you make on this branch afterwards will also be refused. You can now make whatever tailorings you’d like, and commit them safely onto dev. They don’t need to contain the magic nocommit keyword from our pre-push hook. Only the sentinel commit does, and we’ve taken care of that.

Go ahead and change those port numbers or build commands. Add whatever scripts or Dockerfiles you like. Nix-ify the entire build. Go wild. Commit it all.

How to get work done

The dev branch is now your base of operations. You will no longer create topic branches off of main, but will do so off of dev.

$ git checkout dev
$ git checkout -b my-new-feature
# do some work
$ ./run-my-secret-tests.sh
$ git commit -avm "fix thingy"
$ git push # uh oh, how do i push this up? fuck

The workflow is almost the same, but you’ll find git won’t let you push your work up at the end. It contains all your tailorings, plus the feature!

To get stuff done, we’ll have to become a little bit familiar with git rebase. Here’s the magic trick:

$ git rebase dev --onto main

This will take my-new-feature, slice off all the commits since the last tailoring you made on dev, and slide them over onto main. Think of it like lifting it off the dev stuff and plopping it down on main.

This branch is now ready to push, with nobody the wiser.

Pulling down other people’s branches

My colleague just pushed up their own fixes-huge-bug. I want to get all my tailorings back while I look at the branch. How do I do it?

Another dash of rebase:

$ git checkout fixes-huge-bug
$ git rebase dev
# browse through the code however you like now

This will put all our tailorings underneath the commits of fixes-huge-bug.

When I said the dev branch will be our base of operations, I wasn’t kidding.

Updating the dev branch

Your dev branch will have been made against a “snapshot” of main at the time it was put together. Work on main carries on, and we wish to have those changes incorporated. How do we do it?

Captain rebase to the rescue:

$ git checkout main
$ git pull # get latest stuff
$ git checkout dev
$ git rebase main
# dev is now sitting atop the latest main

This will take our tailorings and slide the newest main underneath. This could have merge conflicts, for instance if you’ve modified a local server port for yourself and then somebody did it for real in main.

You’ll need to just resolve them if they happen.

Once this is done, you may wish to update topic branches that you’ve made off dev too:

$ git checkout my-great-fix
$ git rebase dev

Again, this slides in the newest tailorings made on top of the newest main happenings, underneath the commits for my-great-fix.

Adding more tailorings later

You’re working on a feature, and you realize you want to add another change for yourself which would be really handy while writing this feature. How do you do it?

$ git checkout dev
# add some more custom stuff for ourselves
$ git commit -vm "added another great local thing"
$ git checkout - # dash goes back to the branch we were just on
$ git rebase dev

With this we can go back to our dev branch, add the additional tailoring, and then come back to our feature branch and slide in the latest dev work underneath. You’ll need to rebase each feature branch you have locally, in order to get the freshest tailorings incorporated into it.

Summary

This is the dev branch pattern. I use it / abuse it literally all the time. Very few repositories go by without me wanting to make some sorts of adjustments for myself.

I hope this trick helps you be a more productive engineer. Please use and abuse it.