Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git Push: What is the difference between HEAD:refs/heads/<branch> and <branch>?

Tags:

git

push

What does command 1 do that command 2 doesn't?

1. git push <projectpath> HEAD:refs/heads/<branch>
2. git push <projectpath> <branch>

What is the meaning of "HEAD:refs/heads/"?

like image 787
devnull Avatar asked Jul 21 '16 03:07

devnull


People also ask

What are refs and heads in git?

git/refs/ . In Git, a head is a ref that points to the tip (latest commit) of a branch. You can view your repository's heads in the path . git/refs/heads/ . In this path you will find one file for each branch, and the content in each file will be the commit ID of the tip (most recent commit) of that branch.

What is the difference between git push and git push origin head?

In simple words git push command updates the remote repository with local commits. The origin represents a remote name where the user wants to push the changes. git push command push commits made on a local branch to a remote repository.

What is head in git push?

git push origin HEAD:master : This will push your local main branch to the existing remote master branch. git push origin HEAD : This will push your local main branch to the branch of the same name on the remote , in other words, this will create a new branch on the remote called main .

What does refs mean in git?

A ref is an indirect way of referring to a commit. You can think of it as a user-friendly alias for a commit hash. This is Git's internal mechanism of representing branches and tags. Refs are stored as normal text files in the .git/refs directory, where .git is usually called .git .


1 Answers

VonC's answer is correct (and upvoted), but I think another way of looking at this might make more sense.

Note that all of this is assuming that you're using the four-word form of git push, i.e., git push remote refspec. The remote part here is usually just the name origin. We'll define refspec better in a moment.

What git push does

What git push needs to do (and therefore does) is to call up another Git instance on another machine,1 then give that other Git a set of references (usually branch names, sometimes tag names) to update. A reference is simply a name, like master or v1.2, that ideally should be fully-qualified (refs/heads/master or refs/tags/v1.2) so that we can be sure what kind of reference it is—branch, tag, or whatever.

In order for the other Git to update the references your Git hands over, your Git must also hand over some of those big ugly SHA-1 hashes: one per reference. In other words, your Git is going to ask their Git to set their refs/heads/master to, say, ed4f38babf3d81693a68d06cd0f5872093c009f6. (At this point—actually, just a bit before this point, really—your Git and their Git have a conversation about which objects yours want to send them, and which objects they already have, all done by these big ugly hash IDs. Once the two Gits agree about what's going to be sent over, yours does the counting objects and compressing objects and then sends them the objects. The "now, please set some names" part happens nearly last.)

Getting the name and hash parts

Note that there are two parts to your Git's request: (1) a fully-qualified reference, and (2) the big-ugly-hash. (In fact, there's also a third part, the --force flag, but that part is easy and we can just ignore it.) But where does your Git get these?

If you write:

git push origin somename

you've given your Git two pieces of information: the name origin, which your Git uses to look up the URL, and the name somename. Your Git uses this to figure out the full name. Is somename a tag? If so, the full name is refs/tags/somename. Is somename a branch? If so, the full name is refs/heads/somename. Either way works. Of course, you can also write out the full name yourself—and if the name is both a branch and a tag, you may want to do that, rather than letting Git pick one for you.2

So, where does your Git get the big ugly hash? The answer is: from that same name. The name somename, whether it's a branch or a tag, just names some particular Git object. If you want to see the hash yourself, you can do that any time:

git rev-parse somename

will show it to you. This is, in fact, how I got ed4f38babf3d81693a68d06cd0f5872093c009f6: I went to a Git repository for Git and did git rev-parse v2.1.1 and it printed out that hash, because v2.1.1 is a valid tag in any complete copy of the Git repository since version 2.1.1 came out.

Note that when you do use this form—this git push remote name form—Git looks up the name argument in your repository for both purposes: to find out its full name, and to get its hash. It doesn't matter where your HEAD is, only what that full name points to.

But Git does not have to use your branch's (or tag's) ID

The fourth argument to git push is called a refspec, and its syntax actually allows two parts separated by a colon:

 git push origin src:dst

In this case, the dst part supplies the name, but the src part supplies the hash. Git runs the src part through git rev-parse and that produces the hash. So you can:

git push origin mybranch:refs/tags/v42

to create tag v42 in the other Git repository, using whatever commit hash your branch mybranch identifies.

Normally HEAD contains a branch name

In Git, HEAD always names the current commit. Usually it does so by naming a branch, and letting the branch name the commit. So, usually HEAD contains a branch name like master, and a branch name always gets you the tip commit of that branch (that's how Git defines "tip commit"; see the definition of branch in the Git glossary). But always,3HEAD can be turned into a commit:

$ git rev-parse HEAD
2b9288cc90175557766ef33e350e0514470b6ad4

because HEAD is either a branch name (which is then the tip commit), or else you have a "detached HEAD", in which case Git stores the current commit ID directly in HEAD.

Pushing when HEAD is detached

Remember that in order to push, Git needs to get those two pieces of information: the hash, and a (full) name. When HEAD isn't "detached", Git can get both from it: HEAD has a branch name—in the full name form, in fact—and the branch name has the hash. But when you are in "detached HEAD" mode, HEAD only has a hash. Git can't find a branch name in HEAD. There might not be one: you might have checked out a commit by ID, or maybe you checked out by tag name, as in:

$ git checkout v2.1.1

which put you in this "detached HEAD" mode.

In this case, Git demands that you supply both the source hash src—you can still use the name HEAD to get it—and the dst destination name. And, if you use HEAD as the source, Git really needs you to spell out the full destination, because Git can't tell, at this point, if it should be a branch (refs/heads/dst) or a tag (refs/tags/dst).4

Other forms of git push

You can run git push with fewer arguments, e.g.:

git push origin

or even just:

git push

What happens here is that without a refspec, Git consults your push.default setting first. Usually this is simple (the default since Git version 2.0). In this case, Git simply uses HEAD to figure out what to push—which, of course, works only when HEAD is not detached. That's just what we described above.

(Three of the other settings also use HEAD. One of them—the one that was the default before Git version 2.0—does not, but that particular setting proved too error-prone, which is why the default changed. You probably should not use it, at least not unless you are a Git master.)

(And, if you leave out the remote, Git again uses HEAD to figure out where to push to, defaulting, if needed, to origin.)

You can also push multiple refspecs:

git push origin branch1 branch2 tag1 HEAD:refs/tags/tag2

In this case, each refspec is handled in the usual way: get its fully qualified name if needed, so that your Git can give their Git a fully qualified name each time; and look up its hash ID if you didn't use the src:dst form (or if you did use the src:dst form, look up src's ID instead).

You can use wildcards in refspecs:

git push origin 'refs/heads/*:refs/heads/*'

(some shells will eat, mangle, fold, spindle, or mutilate the *s so you may need to use quotes, as in this example; other shells won't—or at least usually won't—but it doesn't hurt to quote). This will push all your branches, or at least try to. This tends to be overly enthusiastic, pushing all your temporary work and experimentation branches, and is probably not what you want, but it's what Git did by default prior to version 2.0.

And, you can use an empty src:

git push origin :refs/heads/deleteme

which is a special-case syntax that means "have my Git ask their Git to delete that reference" (to delete a tag, spell out the tag). As with a detached HEAD, the lack of a fully-qualified name on your side means you should fully-qualify the name for their side. (See footnote 4 again.)

The force flag

If you add --force to your git push command, your Git passes this flag on to their Git. Instead of a polite request—"please, sir, would you like to set your refs/heads/master to ed4f38babf3d81693a68d06cd0f5872093c009f6?"—your Git will send it as a rather insistent demand. Their Git can still refuse either way, but their Git will, by default, do it even if it's not sensible.

Refspecs allow you to control this flag more tightly. The force flag in an individual refspec is a leading plus sign +. For instance, suppose you have new commits for both master and develop branches, and also a new set of rebased commits for experiment, which everyone else has agreed that you are allowed to force-push.

You could do this:

git push origin develop master; git push -f origin experiment

but you can combine it all into one big push:

git push origin develop +experiment master

The leading + on experiment makes that one a command ("update experiment!") while leaving the others as polite requests ("please, sir, if you like, update develop and master").

(This is all a bit esoteric for push, but is actually something you use regularly every day with git fetch, which uses refspecs with + flags to create and update your remote-tracking branches.)


1If the "other repo" is on your same machine and you're using a file:// or local path based URL, this isn't quite true, but the principle is the same and the operations go the same way.

2Better yet, don't get yourself in this situation in the first place. It's very confusing to have one name that is both a branch name and a tag name. (There are similar confusing situations to avoid due to Git's habit of abbreviating: don't name branches with names that resemble remote names, for instance. Git will handle them just fine, but you might not. :-) )

3Actually, there's one exception to this rule, which most people will never notice: when HEAD names an "unborn branch". Mostly this occurs in a new repository, which has no commits at all. Obviously, if there are no commits, there is no commit ID that HEAD could name. It also occurs when you use git checkout --orphan to create a new orphan branch.

4If you use an unqualified name, their Git will look up the name to qualify it. This means you may not know what kind of name you are trying to update or delete. It's generally not a good idea, anyway.

like image 142
torek Avatar answered Oct 13 '22 04:10

torek