Why cannot git reset to pull request

☆樱花仙子☆ 提交于 2020-01-03 03:33:12

问题


Sometimes I log onto a cloud instance, pull a repo, and then want to try a pull request. The command I use now is

git fetch origin pull/<ID>/head && git checkout FETCH_HEAD

which is long. I also tried shorter ways

git reset --hard origin/pull/<ID>
git reset --hard origin/pull/<ID>/head
git reset --hard origin/pull/<ID>/HEAD

which give the following error

$ git reset --hard origin/pull/27
 fatal: ambiguous argument 'origin/pull/27': unknown revision
 or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

Why does git reset --hard origin/<some-branch> work but not the pull request branch?

I notice that in the output of

$ git ls-remote origin

there is a difference between a regular branch and a pull request branch. For example

c31a55 refs/heads/fix-async-stdout-order

615f5a refs/pull/10/head

How does heads differ from pull? (I have shorten the hash here so it's visually cleaner)


回答1:


Every Git repository has its own copies of names. Each name maps to exactly one hash ID, e.g., in your sample:

c31a55 refs/heads/fix-async-stdout-order
615f5a refs/pull/10/head

you've suggested that refs/heads/fix-async-stdout-order maps to hash ID c31a55, and refs/pull/10/head maps to hash ID 615f5a.1

The names are things that Git calls refs or references. (The hash IDs are the hashes that you should be quite familiar with by now.)

In most cases, when you give Git a name, Git just immediately turns it into the underlying hash ID. The hash IDs are what really matter: the names are mainly provided to allow us, mere humans, to deal with the real names that are the hash IDs. The hash IDs never change: they always uniquely identify that particular content—that one specific commit, for instance. The commit and its hash ID are forever, while one or more names can be created or destroyed at will or whim.2

When a name identifies a commit, we can use the name directly to find the commit: refs/heads/fix-async-stdout-order, for instance, identifies commit c31a55. But commits also allow us to find their immediate parent commits: from c31a55 we can work backwards to whatever its parent is, and from that parent we can work backwards another step to another commit, and so on, all the way back to the beginning of time in the repository. So a name like this not only serves to find the one commit whose hash ID it stores, but also all earlier commits in its chain.

When the name is a branch name—as this one is—Git also allows us to use it specially with git checkout. The git checkout command attaches another special name, HEAD, to the branch name. Git now considers us to be "on" the branch, so that if we make new commits, Git will automatically change the hash ID stored under that branch name.

That is, after:

git checkout fix-async-stdout-order

if we do some work and then make a new commit, the name fix-async-stdout-order—which is really refs/heads/fix-async-stdout-order, we've just shortened it for display purposes—will stop pointing to commit c31a55 and start, instead, to point to our new commit. Our new commit will have c31a55 as its parent.

This property, of being able to get "on" a branch, is only allowed for branch names. Git has many names: branch names, tag names, remote-tracking names, and so on. All of them are references, but only references whose spelling starts with refs/heads/ are branch names.

The reference refs/tags/v1.2, if it exists, is the tag named v1.2.

The reference refs/remotes/origin/master, if it exists, is the remote-tracking name origin/master.

Each of these prefixes—refs/heads/, refs/tags/, and refs/remotes/—represents a namespace. References in the refs/heads/ namespace are branch names.

Besides being special in terms of allowing git checkout to use them in the way described above, the other special feature of a branch name occurs when a client Git—such as your own Git—connects to a server Git. The server Git displays, for the client, all of its names, including the branch names, along with the hash IDs that those names represent. The client Git copies down the server's branch names but changes them at the same time, so that what the server calls refs/heads/master, the client calls refs/remotes/origin/master.

This process is how your Git comes to have remote-tracking names in the first place. The server Git has its branches, and your Git comes along—when you run git fetch—and sees and then remembers their branches as your remote-tracking origin/* names. These live in your Git, in the refs/remotes/ namespace.

This process occurs only for branch names!3 Since refs/pull/10/head does not start with refs/heads/, it is not a branch name. The process does not apply to refs/pull/10/head. So that's why and how heads differs from pull.


1These are both abbreviated hash IDs; actual hash IDs are currently always 40 characters long.

2The caveat here is that without a name that allows you to find a commit, or other Git object, that commit or other object is now unprotected from Git's garbage collection process. So not only do names allow us to find the last commit in a chain, they also protect that commit and all of its predecessors from being taken out as trash.

3The process is programmable, through what Git calls refspecs. The description above applies only to the default refspecs that you get when you run git clone. If you make up your own refspecs, you can change what happens here. See the git fetch documentation and note that remote.origin.fetch is a cumulative setting; each instance of remote.origin.fetch provides one refspec.

The default at clone time is for Git to create one remote.origin.fetch setting, that either copies all of their branches to your remote-tracking names, or copies one of their branches to one remote-tracking name if you opted for --single-branch during cloning.




回答2:


GitHub suggests: git fetch origin pull/ID/head:BRANCHNAME: that way you are controlling the local branch naming convention, instead of creating (due to the default refspec on GitHub for PR)

But you have more advance techniques here:

[alias]
copr = "!f() { git fetch -fu ${2:-origin} refs/pull/$1/head:pr/$1 &&
                    git checkout pr/$1; }; f"

And then:

$ git copr 1234            # gets and switches to pr/1234 from origin
$ git copr 789 upstream    # gets and switches to pr/789 from upstream

hub is also known for wrapping the PR import.



来源:https://stackoverflow.com/questions/54996024/why-cannot-git-reset-to-pull-request

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!