Arvids Blog

Thoughts on programming and more

Gerrit: Detached HEAD Workflow

Gerrit is a code hosting system similar to GitLab or GitHub, focusing on a better review experience rather than integrating with every other service on the planet and neglecting the core experience of their product.

Since GitLab, GitHub, and their predecessors have been so prevalent in the broader software development community, Gerrit requires a bit of rethinking of interacting with your distributed version control system. Behind the scenes, Gerrit is still using Git, but unlike many other systems, it’s not using a branch-based workflow for submitting changes to the mainline1.

This blog post aims to introduce an alternative working mode with Git and Gerrit called “Detached HEAD”. But before diving into that, let’s provide some background.

In Git, the HEAD is a pointer that points to the currently checked-out commit or branch. It’s Git’s way of knowing which commit you’re looking at in your local working tree. HEAD is a file that points to the commit.

$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
3148fa73f6664f75746477188c7951a495252821

In the example, HEAD is pointing at the ref/heads of master. The ref/heads/master file points to the latest commit on master.

The HEAD can be in multiple states: attached and detached. Typically, you’re working with an attached HEAD. That means your HEAD points to a branch (for example, master). Whenever you’re committing work, the commit is recorded on the current branch HEAD is currently referencing. A detached HEAD is whenever you haven’t checked out any branch, and all commits you’re making in this state are getting “lost” once you’re checking out a branch (don’t worry, you can recover them).

Many publications erroneously frame the “detached HEAD” state as an error state that one must recover from, highlighting the lack of education about Git as a tool and how to use it effectively.

With that out of the way, let’s get started. First, we’re preparing our working tree:

$ git checkout master # our work is based on the master branch
$ git checkout --detach # we're now in a detached `HEAD` state

We’re now in a detached HEAD state. So you can work as you usually would by creating commits using git commit or git commit --amend.

Note: In Gerrit, every commit is a change. If you’re pushing multiple commits, Gerrit will create the changes in a chain of dependencies. You can update a change by amending the commit and pushing it. Gerrit will create a new revision automatically.

$ # Do some work, and create a README.md
$ git add README.md
$ git commit -a

Once you’re ready to push your changes, you need to rebase your work on the master branch and push it.

$ git checkout master
$ git pull --rebase 
$ git checkout -
$ git rebase master

When you were checking out the master branch from your detached HEAD state, you got a scary message that you’re leaving behind commits that are not connected to any branch. That’s the state I mentioned earlier. You can ignore the warning in this context since we are switching back to the commit with git checkout - (similar to how cd - works).

Your change is now rebased and ready to be pushed for code review to Gerrit.

$ git push origin HEAD:refs/for/master

Gerrit has created a new change for every commit not on the master branch.

You can now continue working in this detached HEAD state and add further commits or update existing commits.

$ git add -A
$ git commit -a
$ git push origin HEAD:refs/for/master

Gerrit will now create a new change that depends on the change pushed earlier. That’s how you can create dependency chains between submitted changes, allowing you to keep each change as small as necessary.

Congratulations, you should now be ready to start working with Gerrit.

For a more in-depth introduction to Gerrit, I recommend reading the Gerrit User Guide.

Cheatsheet

Starting a new change

This assumes that you pushed your last change to Gerrit. If you didn’t, save your change first (see here).

$ git checkout master
$ git pull --rebase 
$ git checkout --detach

You can now commit as usual.

Saving a change

You can save your current working state by creating a new named branch.

$ git checkout -b your-branch-name

Now you can start working on a new change.

Pushing a change

To push a change to Gerrit, you need to push to refs/for/<branch>. It would be best if you also rebased your change on the target branch before pushing, see here.

$ git push origin HEAD:refs/for/master
Rebasing a change on master
$ git checkout master
$ git pull --rebase 
$ git checkout -
$ git rebase master

Fetching a change

To fetch a change:

  1. Navigate to the change on Gerrit.
  2. Click the “Download” button on the top right of the list of files in the patchset to open the “Download” overlay.
  3. Copy the “Checkout” line and execute it in your terminal.

It’ll put you into a detached HEAD state, and you don’t have to do anything else. You can start working like you usually would.

Rebasing a change on top of a patchset

Like fetching a change from Gerrit, copy the “Checkout” line and remove the last && git checkout FETCH_HEAD.

$ git fetch ssh://username@host:29418/myProject refs/changes/74/67374/2
$ git rebase -i FETCH_HEAD

Updating a change

Again, like fetching a change from Gerrit, copy the “Checkout” line.

$ git fetch ssh://username@host:29418/myProject refs/changes/74/67374/2 && git checkout FETCH_HEAD
$ # fix/update whatever you need
$ git add -A
$ git commit --amend # Make sure to only amend, don't create new commits
$ git push origin HEAD:refs/for/master
$ git checkout - # switch back to your old change
1

If you’ve used Perforce in the past, you might be much more familiar with a branchless workflow.