There are several things happening all more or less simultaneously, which can be pretty confusing. The way to keep this all straight is to start with this notion: Git is not about files. Git is about commits. Commits contain files, so as a result, Git will store files; but what Git cares about are the commits, with their big ugly hash IDs.
You've seen commit hash IDs in git log
output, probably. They look like bc12974a897308fd3254cf0cc90319078fe45eea
, for instance—this is a commit in the Git repository for Git itself. Every commit gets its own unique hash ID. No other commit anywhere, ever, can use that hash ID. Git took that one for itself and now it's that commit. All of your commits will have different hash IDs.1 Every Git everywhere automatically agrees with every other Git everywhere what the hash ID is for each commit once it's made, and no commit hash ID ever changes—so bc12974a8973...
is always, forever, that particular commit in the Git repository for Git.
Branch names, like fb1
or fb3
,2 come into the picture because mere humans can't remember, or type in, big ugly hash IDs. I had to cut-and-paste that one to get it right. These names let you—and Git—find the hash IDs.
You can create and destroy branches at any time. To create a branch, you pick out some commit—some hash ID, which you can find by some other name or by running git log
and cutting and pasting a raw hash ID—and tell Git: Make a new name, and in that name, store this here raw hash ID:
git branch gitty bc12974a897308fd3254cf0cc90319078fe45eea
assigns the above hash ID to the name gitty
. (Your Git will ensure that this hash ID actually exists in your repository, and is the hash ID of a commit.3) If you use another name:
git branch gitty master
Git first turns the name master
into the raw hash ID, then puts the raw hash ID into the new name gitty
.
To see what hash ID any name represents at the moment, use git rev-parse
:
$ git rev-parse master
bc12974a897308fd3254cf0cc90319078fe45eea
That's how I got the big ugly hash ID above in the first place.
1Technically, it is allowed to re-use hash IDs, as long as the two Gits with re-used IDs never meet each other. So if you never try to combine a Git repository for Git with your own Git repository for whatever it is you're doing, your Git could re-use that hash ID. The chance of an actual hash collision is extremely remote, except in cases in which someone deliberately forces a collision. See also How does the newly found SHA-1 collision affect Git?
2I put these in all lowercase. In general, it's unwise to mix upper and lower case names for branch and file names, if you want your repository to work right on most MacOS and Windows systems.
3Git uses hash IDs for more than just commits. They also identify the specific contents of specific versions of files, for instance. Hence when you make thousands of commits that mostly re-use most of the files, the new commits just refer back to the existing, already-committed files.
Draw a picture
Even though these hash IDs are really concrete, they're not useful for humans: we just can't keep them all straight. Again, that's why we use branch names. But inside Git, Git is using the hash IDs. This will make more sense in your own head if you draw pictures.
Remember that each commit holds a snapshot—a set of files, as of however they looked at the time you made that commit—but it also holds some extra information, which Git calls metadata. This metadata includes the name and email address of the person who made the commit, and the date-and-time stamp for the commit. It includes the log message: click on the link to the GitHub copy of that Git commit and you'll see that its subject line is Fourth batch
(and the rest is just a signoff line—not the best commit message ever, but it could be worse4). Perhaps the most important part of commit metadata, though, is that each commit holds the raw hash IDs of its parent commits.
When one thing holds the hash ID of another commit, we say that the first thing points to the second commit. So if the name fb1
holds the hash ID of a commit, the branch name fb1
points to the commit. That commit itself holds the hash ID of some parent commit. The parent commit holds the hash ID of its parent, and so on:
... <--o <--o <--o <--fb1
where each round o
represents a commit.
These arrows all go only one way—backwards—so by definition, a branch name points to the last commit in a chain of commits. The commits themselves have the internal backwards arrows that link them together.
When you make a branch and start committing, what Git does is make the new commits point back to the older commits. Then it moves the branch name forward so that it points to the new commit. Let's draw that, using single uppercase letters to stand in for commit hash IDs (since hash IDs are too big and ugly):
...-F--G--H <-- master, fb1 (HEAD)
I'll describe what the word (HEAD)
is doing here in just a moment. Here, for my convenience, I've drawn all the internal arrows as just connecting lines. We can just keep in the backs of our minds that they point backwards; most of the time we don't really have to care.
We've just created fb1
from master
, so both names point to the same commit, the one labeled H
(for hash ID) here. Note that all the commits are on both branches: this is a weird thing that Git does, that other version control systems don't do. Commits are on more than one branch at a time.
Now let's make a new commit, which we'll call I
. We do the usual mess-with-some-files step, run git add
on them, and run git commit
and supply Git with a log message. Git packages up a new snapshot (of all files, not just the ones we changed) and sets the new commit's parent to be H
, so that new commit I
points back to H
:
...-F--G--H <-- master, fb1 (HEAD)
\
I
Now Git updates a branch name to point to I
. Which branch name does Git update? That's where HEAD
comes in. HEAD
tells Git which branch we have checked out. We have fb1
checked out, and hence we had commit H
checked out. Now that Git has created new commit I
, Git updates the name to which HEAD
is attached, so that we get:
...-F--G--H <-- master
\
I <-- fb1 (HEAD)
We've now created a new commit on the branch fb1
. Commits up through H
are still on both branches. Commit I
, the new one, is only on fb1
.
If we make more commits, those too are only on fb1
:
...-F--G--H <-- master
\
I--J <-- fb1 (HEAD)
4In the xkcd comic, newer commits are towards the bottom. In git log --graph --oneline
output, newer commits are towards the top. In the way I draw commit graph fragments on StackOverflow, newer commits are towards the right. Graph drawings can be oriented any way that helps you view them, so pick whatever way suits you.
Branches are great, let's make more!
Now that we have fb1
with a couple of commits on it, let's go back to master
and make a new branch fb2
. We can do that in any number of ways, including:
git checkout master
git branch fb2
git checkout fb2
or more simply:
git checkout -b fb2 master
which does all of that in one step. The result is that our graph doesn't change at all, but we add a new name, fb2
, and attach HEAD
there:
...-F--G--H <-- master, fb2 (HEAD)
\
I--J <-- fb1
We now have commit H
checked out, through the new name fb2
. If we run git log
, we'll see commit H
, then commit G
, then commit F
, and so on: git log
starts where we are, then follows the internal arrows backwards.
Note that commits I
and J
are still in our repository, and we can still find them: the name fb1
finds commit J
, and from commit J
, we can step back across the internal, backwards-pointing arrow to find I
. A plain git log
won't show these two commits: we have to run git log fb1
to see them, so that git log
will start with commit J
as identified by the name fb1
, rather than starting with HEAD
and therefore finding H
.
Now that we're in this new position, let's make two new commits. They'll get their own unique, big ugly hash IDs, but we'll just call them K
and L
. Let's draw them—and for convenience let's flip fb1
to the top row:
I--J <-- fb1
/
...-F--G--H <-- master
\
K--L <-- fb2 (HEAD)
We now have three branches in our own Git repository, found by the three names fb1
, fb2
, and master
. These three branches find three commits, but those three commits themselves each find more commits: Git starts with whichever commit we tell it, then walks backwards, following the internal, backwards-pointing arrows.
Because commit H
is the parent of both I
and K
, we find commit H
three times. We find its parent, G
, three times too, and so on for all the commits behind it. All of these commits are on all three branches now. But commits I-J
are only on branch fb1
, and commits K-L
are only on branch fb2
.
Deleting names
Suppose we were to delete the name master
entirely, using git branch -d master
:
I--J <-- fb1
/
...-F--G--H
\
K--L <-- fb2 (HEAD)
We now have no branch named master
. But we still have all of its commits. They're still easy to find: we can start at either fb1
(commit J
) or fb2
(commit L
) and walk back to H
, and there's H
. So all of these commits are still there, still on two branches. They were on three branches before; now they're on two.
We can put master
back, using git branch master <hash-of-H>
(cutting and pasting the hash ID from git log
), then tell Git to delete the name fb1
. We can't, right now anyway, delete fb2
at all because we're standing on it (HEAD
is attached to fb2
). We would have to tell Git to force the deletion of fb1
, because look what happens if we did:
I--J ???
/
...-F--G--H <-- master
\
K--L <-- fb2 (HEAD)
Once the name fb1
is gone, how will we ever find commit J
? We depend on finding commit J
to find commit I
, too. We can still find L
and K
and H
and so on, but if we delete the name fb1
, we lose the hash ID for the last commit in that branch.
Since Git is all about commits, Git won't let us delete fb1
without forcing it, and of course, we shouldn't. However, if fb1
still pointed to H
—so that we could find commit H
via fb2
—Git would let us delete fb1
without forcing it.
Using another Git: git fetch
All of the above describes how we use our Git repository, locally. But in your case, you have another Git repository, stored over on GitHub. Your Git calls this other Git origin
—that's basically a short name for the URL you use to have your Git talk to the Git over on GitHub.
You can run:
git fetch origin
to have your Git call up their Git. Your Git uses the URL stored in the name origin
—Git calls this thing a remote and it's just a short name for a URL and some other useful data—to phone up their Git, and your Git and their Git have a conversation. Their Git lists all their branch names, and their raw commit hash IDs. Your Git checks to see if you already have these commits, or not. If you don't have the commits—remember, the commit hash IDs are universal across every Git, so your Git can just check by ID—your Git will then ask their Git to package up those commits. By extension, your Git will get all of those commits' files, and their parent commits and their files, and so on. Your Git and their Git talk with each other to figure out which commits you already have, and therefore don't need, so that they only send you the stuff you need.
Having sent you all those new commits—whatever their hash IDs are—your Git now has them, stored. But your Git needs to have some sort of name for the last new commit in any updated branch. This is where your remote-tracking names come in.
Let's say, for instance, that they've added a commit to their master. We'll call this commit M
:
I--J <-- fb1
/
...-F--G--H <-- master
\
\--M <-- ???
\
K--L <-- fb2 (HEAD)
What name is your Git going to use to remember their master
? The answer here is that your Git creates your own name origin/master
, in a separate name-space,5 so that we get:
I--J <-- fb1
/
...-F--G--H <-- master
\
\--M <-- origin/master
\
K--L <-- fb2 (HEAD)
In fact, you already had an origin/master
pointing to H
before. We just didn't draw it in!
Now, if you created an fb1
in the Git over at origin
and you've done a git fetch
, you'll also get an origin/fb1
. This will point to whichever commit the fb1
on the GitHub Git points to.
If someone else, who can also manipulate the GitHub Git, moves or deletes their fb1
, your Git still has your own fb1
. That never goes away on its own: it is your branch, and you control it. Your origin/fb1
might move, and if you set your Git up to prune remote-tracking names, your origin/fb1
might go away entirely. But your fb1
, your branch, is under your control. You choose how to move it or delete it.
5The full name of a branch in your repository starts with refs/heads/
. The full name of the remote-tracking name origin/master
is refs/remotes/origin/master
. This doesn't start with refs/heads/
, so you can create your own local branch name, refs/heads/origin/master
, which is separate from your remote-tracking name.
Don't do this! It won't break Git but it might break you. :-) If you have accidentally done this, rename your local branch so that its name does not start with origin/
any more, so as to de-confuse things.
What about git push
?
The git push
command is basically the opposite of git fetch
: you have your Git call up their Git, and then instead of getting new commits from them, you give new commits to them.
It's not completely opposite though. When you get new commits from them, your Git automatically creates or updates your remote-tracking names, so that you have the origin/*
names by which to find their new commits. But when you git push
commits to them, you ask them to set their branch names.
So if you git push
commits I
and J
, for instance, you will almost certainly want to ask them to set their branch name fb1
to match yours.
Let's say we have commit M
and we've already updated our own master
to include it. They have fb1
pointing to commit H
, and don't have fb2
at all yet, so that we have, in our repository, this graph:
I--J <-- fb1
/
...-F--G--H--M <-- master, origin/master, origin/fb1
\
K--L <-- fb2 (HEAD)
We can now run:
git push origin fb1
This will send commits I
and J
to them: they don't have them, but they need them in order to move their fb1
to point to J
. Then our Git will ask their Git: Please, if it's OK, set your fb1
now to point to commit J
.
In this case it will be OK. They will set their fb1
to point to J
. Your Git will acknowledge this by updating your own origin/fb1
, and now you have:
I--J <-- fb1, origin/fb1
/
...-F--G--H--M <-- master, origin/master
\
K--L <-- fb2 (HEAD)
in your repository. They just have master
identifying M
and fb1
identifying J
; they don't have fb2
at all yet.
What about git pull
?
The git pull
command is just shorthand for:
- run
git fetch
;
- then run a second Git command, usually
git merge
, but you can choose git rebase
.
I advise newcomers to Git to avoid git pull
because it hides this two-step process, and leaves them stranded when the second command fails for some reason (which can and does happen).
Merge is a complicated command, with complicated failure modes.
Rebase is in some sense worse: in effect, it is a repeated series of git cherry-pick
commands (followed by one branch-label move), and each one is a form of merge. So if merge can fail, so can each cherry-pick. If there are five commits to cherry-pick to accomplish the rebase, there are five steps that can fail!
Most merges actually do succeed on their own, and a significant number of rebases work without problem. But if one does fail, it's important to know which one you were doing, so that you know where to look for help.
It's OK to use git pull
—as a convenience instead of git fetch
followed by whichever second command—but just remember that that's what it is, so that when the second command fails, you know which second command you used, and what to do next. Even knowing all this, I mostly don't use git pull
myself, because I often like to run git log
between the git fetch
and the second command, to look at what git fetch
fetched. Sometimes, that changes whether I use git merge
or git rebase
!
Conclusion
With all of the above in mind, let's look at the subject line question:
What happens to local committed code if git branch is deleted on server?
Nothing. Nothing at all happens to local commits. Your local branches are yours, to do with as you will. You choose what happens next.