UP | HOME

Tracking Review Branches with Git

Table of Contents

When reviewing code, it's often necessary to pull the changes under review. There are ad-hoc ways of doing this. But there are also more automatic ways as well. In this post, we discuss the more automatic approaches for GitHub and GitLab. Other forges may have the ability to use this trick, we discuss how to discover the remote references, if they are available.

Terminology: I am going to use "pull request" to refer to GitHub's "pull requests" and GitLab's "merge requests." The difference in naming is purely cosmetic, both represent the same concept, and both borrow that concept from kernel development etiquette, what https://git-scm.com/ was originally developed for.

GitHub

If we are working with a GitHub repository, we can run the following command, assuming origin is the remote where pull requests are submitted:

git config --add remote.origin.fetch "+refs/pull/*/head:refs/remotes/origin/pull/*"

This command adds a line to the remote.origin configuration section of the current repository. The fetch value provides a remote to local naming pattern to be used when fetching changes from the remote.

The full remote.origin section should look similar to the following:

[remote "origin"]
	fetch = +refs/heads/*:refs/remotes/origin/*
	url = git@github.com:owner/repo.git
	fetch = +refs/pull/*/head:refs/remotes/origin/pull/*

GitLab

Similarly, if we are working with GitLab repositories, we can use a similar command to accomplish the same result, again assuming origin as the remote:

git config --add remote.origin.fetch "+refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*"

Likewise, the full remote.origin configuration should be similar to the following:

[remote "origin"]
  url = https://gitlab.com/owner/repo.git
  fetch = +refs/heads/*:refs/remotes/origin/*
  fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*

Local Branch Prefix

The local branch prefix can be set to any value that is desired. Don't like origin/pull/${pr}? Change it. A common option is to use origin/pr instead. Similarly, don't like origin/merge-requests/${pr}? Feel free to change the reference prefix. Simply modify the commands above to use the appropriate prefix for each remote:

git config --add remote.origin.fetch "+refs/pull/*/head:refs/remotes/origin/pr/*"
git config --add remote.origin.fetch "+refs/merge-requests/*/head:refs/remotes/origin/pr/*"

When choosing a prefix, it's important not to choose a prefix for pull requests that are often also used for branches on the remote. For example, if developers often push branches to the project such as pr/1337, https://git-scm.com/ warns that these references are ambiguous.

Branch Discovery

When using a different forge than GitHub or GitLab, it may be possible to discover the appropriate pattern for references to pull requests. Documentation for checking out pull request references may not be available, unlike GitHub and GitLab. Therefore, we need to look to git itself, specifically, git-ls-remote.

Using git-ls-remote, we can list all remote references hosted by the repository. git-branch, by comparison, only lists references which are being "tracked," i.e., fetched by the fetch rules for the remote repository.

git ls-remote

From the list of references, it should be evident which references refer to pull requests. Unfortunately, there is no immediate way to select between open and closed pull requests. For GitHub, this means git ls-remote returns every pull requests for the repository.

The following is an example listing from nix-community/emacs-overlay:

c30763fa3d800911ee57c4cdaf558b416e4c5780	HEAD
5b9f3a8258c4b42c9c3dfcb77bde5d2e3abbc6eb	refs/heads/elpa
6cce77a7865731bd64f12b0ade938c0082f869c6	refs/heads/expect
47a2e0b85642a229aa45c03c4c590ad0e4294788	refs/heads/gccemacs
c30763fa3d800911ee57c4cdaf558b416e4c5780	refs/heads/master
80b60da21dbf15a683a3fcc0138a96478794bef0	refs/heads/melpa
b74dbf73ecf7cc420740c87af4c2d113d1fdd58e	refs/heads/nativecomp-pkgs
f911b18763ad50bf49a5928b95fb149ba638b2e2	refs/heads/updater-python
3d7612182992cc57148c545c11cb9415bb7d23f0	refs/pull/103/head
2ecd882d2b6e2f4f710c08419a5899d8215f7cc9	refs/pull/103/merge
7689a4093e2986e72a2b3e981b4ae86124c1dfb6	refs/pull/105/head
d7bdc8d3be125e90c8f01650e17b66b814609ec3	refs/pull/111/head
9e8e8cae4dba0fe46569f010119b4ca269a3a9c0	refs/pull/118/head
401546016dc12b1416f5a3f85e26e1b8294ee5c4	refs/pull/119/head
997966c8d635dcada8c8bd3aa40a643745dd5ca8	refs/pull/12/head
f911b18763ad50bf49a5928b95fb149ba638b2e2	refs/pull/121/head
5a99e863b84825a13a125b145170b8d6aeef9200	refs/pull/121/merge

From the above list, we can see the pull requests are under the refs/pull path.

Manual Checkout

Without downloading and keeping track of all pull request branches, individual pull requests can be checked out as well. However, doing so requires an extra command.

git fetch origin pull/${pr}/head
git checkout -b pr/${pr} FETCH_HEAD

GitHub automatically creates a merge reference between the pull request and the merge target branch. This is done to check whether a pull request can be merged cleanly or if merge conflicts exist. Following similar steps, we can checkout GitHub's automatically merged pull request reference:

git fetch origin pull/${pr}/merge
git checkout -b pr/${pr}/merge FETCH_HEAD

Repositories with Many Pull Requests

While adding the extra fetch and branch prefix is a nice way to automatically track pull requests against a repository, the repository may have more pull requests than we wish to regularly fetch and update. Consider NixOS/nixpkgs. As of this writing, the number of open pull requests is 2,540. Furthermore, there has been 101,345 pull requests submitted to date. 49 pull requests were opened today alone. Running git remote update or git fetch upstream with the added fetch for pull requests might be to demanding for a regular workflow.

Instead of always downloading pull request references, instead, we can fetch the pull requests from a different "remote."

For example, the following is the relevant sections of my .git/config for NixOS/nixpkgs:

[remote "origin"]
	url = ssh://github.com/kennyballou/nixpkgs.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[remote "upstream"]
	url = git://github.com/nixos/nixpkgs.git
	fetch = +refs/heads/*:refs/remotes/upstream/*
[remote "review"]
	url = git://github.com/nixos/nixpkgs.git
	fetch = +refs/pull/*/head:refs/remotes/review/*

This way, git fetch upstream only fetches upstream branch changes; no pull request references are fetched. Conversely, git fetch review fetches references for all pull requests. This enables rebase workflows to still be relatively fast, while review workflows can still be accomplished using the same fetch trick as above.

tl;dr:

To automatically fetch pull requests from GitHub repositories, run the following command, assuming upstream is the remote where pull requests are submitted:

git config --add remote.upstream.fetch "+refs/pull/*/head:refs/remotes/upstream/pr/*"

Or, for GitLab repositories, under the same assumption:

git config --add remote.upstream.fetch "+refs/merge-requests/*/head:refs/remotes/upstream/pr/*"

To manually checkout a pull request branch, use the following:

git fetch upstream pull/${pr}/head
git checkout -b pr/${pr} FETCH_HEAD

Finally, if the repository contains a large number of pull requests, it may be preferred to use a different "remote" for tracking pull request references:

git remote add review $(git config remote.upstream.url)
git remote review.upstream.fetch "+refs/pull/*/head:refs/remotes/review/*"