On GitHub, I normally clone someone's fork or add it as a remote to download their changes. However, if the branch/fork is completely deleted, this isn't an option.
Yet, you are still able to view the changed files in the PR if you click the "Files changed" tab in the PR, even with the fork/branch deleted and the PR closed.
Is there a way to get a branch with those changes locally on my system? One idea would be to just manually recreate everything by copy-pasting the changes from the website, but this approach seems sub-optimal.
Yes, it's relatively easy:
git fetch <remote-or-url> refs/pull/1234/head:refs/heads/pr1234
for instance will turn their (GitHub's) pull-request-#1234 commit(s) into your pr1234
branch.
The way this works is straightforward:
All names1 in Git are refs or references. That is, the branch name master
is really just refs/heads/master
; the tag name v1.2
is really just refs/tags/v1.2
; your remote-tracking origin/master
is really just refs/remotes/origin/master
. Names that are branch names start with refs/heads/
. Git normally strips off the leading part of whatever kind of name it is, on the assumption that you'll know that master
is a branch, v1.2
is a tag, and origin/master
is a remote-tracking name.
When you run git fetch
, your Git obtains their commits (and other objects) by the name-and-hash-ID pairs that their Git shows to your Git. Then it copies over any commits (and other objects) needed, so that your Git can update your remote-tracking name(s) and/or write entries into your Git's .git/FETCH_HEAD
file and so on. Last, it does a couple of things:
.git/FETCH_HEAD
.
The FETCH_HEAD
file is left behind for the git pull
code, mainly; it's a bit of a historical artifact now that git pull
has been rewritten in C.
The nice thing about refs is that, being divided into namespaces, you or anyone can invent your own. The refs/heads/
and refs/tags/
and refs/remotes/
spaces are taken, as are refs/bisect/
and refs/replace/
and some others, but GitHub were able to use refs/pull/
, which Git itself does not, to store their names.
In this refs/pull/
space, GitHub insert the PR or issue number (these numbers are per-repository and otherwise global to the repository) followed by /head
for the tip commit and /merge
for the result of performing an unattended, automatic test git merge
. If the automatic merge fails, GitHub simply do not create the merge
ref. Hence if there is a PR with number 1234, refs/pull/1234/head
exists; if that PR was successfully test-merged, refs/pull/1234/merge
also exists, and if not, it does not.
Note that the conversion from their (GitHub's) branch names to your remote-tracking names is controlled by your own remote.origin.fetch
configuration line, which defaults to:
+refs/heads/*:refs/remotes/origin/*
This is a refspec: a pair of refs, separated by a colon, and optionally prefixed with a plus sign +
(always used for this particular case). That matches all of their branch names, as the left side or source pattern matches branch names. It causes your Git to replace their branch names with your remote-tracking names, as the right side or destination pattern replaces refs/heads/
with refs/remotes/origin/
(keeping everything matched by *
intact).
This means you can add:
+refs/pull/*/head:refs/heads/pr*
to your remote.origin.fetch
refspecs, in your .git/config
, so that git fetch
will automatically, and almost always,2 create or update your prnumber
branches. It will, however, potentially wreck any of your own personal prnumber
branches that you created on your own, so be aware that if you do this, you must avoid naming branches pr
-something (at least something numeric, but beware pruning; see footnote 2).
1The exceptions here are HEAD
, MERGE_HEAD
, CHERRY_PICK_HEAD
, and the like. While FETCH_HEAD
works like a reference sometimes, the FETCH_HEAD
file is particularly special in that it can contain multiple lines and some extra annotations; the others contain only (and exactly) one hash ID or one symbolic ref.
2"Always" is too strong, and even my slightly weasel worded almost always might be as well: any time you run git fetch
with some refspec argument(s), that will override this default. Your configured remote.origin.fetch
settings are used only if you did not override them. Remember how this interacts with fetch.prune
or git fetch -p
, too: the *
in the destination now means Git will automatically remove matching refs that weren't written by the fetch action.
The ability to write pr*
rather than, e.g., pr/*
, depends on your Git vintage. In some older versions of Git, glob-like *
patterns in refspecs can only be used in "whole name component" ways, e.g., refs/heads/*:refs/remotes/origin/*
is always OK in all Git versions, no matter how ancient, but sometimes refs/heads/a*:refs/remotes/origin/a*
is not.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With