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/*"