We’ve seen how to create local branches that you do work on, then merge back into the main branch, then git push it up to a remote server.
This part of the guide is going to try to clarify what’s actually going on behind the scenes, as well as give us a way to push our local branches to a remote for safe keeping.
First, a refresher!
Recall that the remote repo you cloned yours from is a complete copy of your repo. The remote repo has a main branch, and therefore your clone also has a main branch.
That’s right! When you make a GitHub repo and then clone it, there are two main branches!
How do we differentiate them?
Well, on your local clone, we just refer to branches by their plain name. When we say main or topic2, we mean the local branch by that name on our repo.
If we want to talk about a branch on a remote, we have to give the remote name along with the branch using that slash notation we’ve already seen:
main # main branch on your local repo
origin/main # main branch on the remote named origin
upstream/main # main branch on the remote named upstream
zork/mailbox # mailbox branch on the remote named zork
mailbox # mailbox branch on your local repoImportantly, not only do the words origin/main refer to the main branch on origin in casual conversation, but you actually have a branch on your local repo called origin/main.
This is called a remote-tracking branch. It’s your local copy of the main branch on the remote. You can’t move your local origin/main branch directly; Git does it for you as a matter of course when you interact with the remote (e.g. when you push or pull).
We’re going to call the main branch on our local machines the local branch, and we’ll call the one on origin the upstream branch.
And this is going to get confusing later when we name a remote
upstreamin a way that has nothing to do with the upstream branch terminology we’re using here.
I want to go over that one more time to drive it home.
Let’s say you have these two branches on your computer because you’ve just cloned the remote repo at the origin:
main # main branch on your local repo
origin/main # main branch on the remote named originWhen you have those two branches on your computer, there are actually three branches in the world.
main on your computer.origin/main on your computer.main on the origin computer, usually a different computer than yours, e.g. one at GitHub or something.Notice that the first two of these are on the repo on your computer!
The branch origin/main is just where your computer thinks that main on the origin is. Your computer got this information the last time you pulled or fetched from origin.
If other people have pushed to main on origin since your last pull, your origin/main on your local computer won’t be up to date.
And normally you don’t have to worry about this much; when you try to push, Git will tell you if someone else has pushed changes in the meantime and you have to pull first to update your origin/main branch. No biggie.
But I wanted to spell that out so you had a more complete mental model of what’s happening behind the curtain, here.
Remember how git branch listed the branches you had? Let’s power it up so you can see all the remote tracking branches, as well. This will help in the following sections.
Basically, we just give it the -avv switch for “all” (to list the remote tracking branches) and “verbose” (to give info about which commits they’re pointing to) and “verbose” again (to give info about which remote branches map to which remote branches.
Here’s the result for the repo that holds the source for this book:
% git branch -avv
* main 2d63af5 [origin/main] indexing
sphinx cdac325 [origin/sphinx] partial port
remotes/origin/HEAD -> origin/main
remotes/origin/main 2d63af5 indexing
remotes/origin/sphinx cdac325 partial portWe see my two local branches (main and sphinx). Looking on those two top lines, you see remote tracking branches in brackets (origin/main and origin/sphinx). When I push or pull from main or sphinx, those are the remote tracking branches that are merged to.
Additionally, we see information about the remote below that.
The first line about remotes/origin/HEAD is a little strange. It just points to origin/main which simply lets us know that main is the initial branch for the repo that Git will use when you clone it. You typically don’t need to think about this line.
The remaining two lines tell us what commits the remote tracking branches origin/main and origin/sphinx are pointing at. Looking closely, we see they’re pointing to the same commits as our local main and sphinx indicating that everything is in sync. (As far as we know—someone else might have pushed something to the repo since our last pull and we don’t know about that yet.)
Fun Fact: when you push or pull, you technically specify the remote and the branch you want to use. This is me saying, “Push the branch I’m on right now (presumably main) and merge it into main on origin.
$ git push origin main“But wait! I haven’t been doing that!”
It turns out there’s an option you can set to make it happen automatically. Let’s say you’re on the main branch and then run this:
$ git push --set-upstream origin main
$ git push -u origin main # same thing, shorthandThis will do a couple things:
main to the remote server (that’s the push origin main part).origin/main is tracking your local main branch (that’s the -u part).And then, from then on, from the main branch, you can just:
$ git pushand it’ll automatically push to origin/main thanks to your earlier usage of --set-upstream.
And git pull has the same option, as well, though you only need to do it once with either push or pull.
“But wait! I’ve never used --set-upstream, either!”
That’s because by default when you clone a repo, Git automagically sets up a local branch to track the main branch on the remote.
Depending on how you made your repo, you might also have a reference to
origin/HEAD. It might be weird to think that there’s aHEADref on a remote server that you can see, but in this case it’s just referring to the branch that you’ll be checking out by default when you clone the repo.
“OK, so what you’re telling me is that I can just git push and git pull like always and just ignore everything you wrote in this section?”
Well… yes. Ish. No. We’re going to make use of this to push other branches to the remote!
I’m going to make a new local branch topic99:
$ git switch -c topic99
Switched to a new branch 'topic99'And make some changes:
$ vim README.md # Create and edit a README
$ git add README.md
$ git commit -m "Some important additions"In our log, we can see where all the branches are:
commit 79ddba75b144bad89e1cbd862e5f3b3409f6c498 (HEAD -> topic99)
Author: User Name <user@example.com>
Date: Fri Feb 16 16:44:50 2024 -0800
Some important additions
commit 3be2ad2c31b627b431af8c8e592c01f4b989d621 (origin/main, main)
Author: User Name <user@example.com>
Date: Fri Feb 16 16:14:13 2024 -0800
Initial checkinHEAD refers to topic99, and that’s one commit ahead of main (local) and main (upstream on the origin remote), as far as we know. And we know this because it’s one commit ahead of our remote-tracking branch origin/main.
Now let’s push!
$ git push
fatal: The current branch topic99 has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin topic99
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.Ouch. The short of all this is that we said “push”, and Git said, “To what? You haven’t associated this branch with anything on the remote!”
And we haven’t. There’s no origin/topic99 remote-tracking branch, and certainly no topic99 branch on that remote. Yet.
The fix is easy enough—Git already told us what to do.
$ git push --set-upstream origin topic99And that will do it.
At this point, assuming you’ve pushed to GitHub, you could go to your GitHub page for the project, and near the top left you should see something that looks like Figure 10.1.
If you pull down that main button, you’ll see topic99 there as well. You can select either branch and view it in the GitHub interface.
There are few things that can happen here.
Someone deletes the branch on the remote, but your corresponding remote tracking branch (the one on your clone) still exists and you want to delete it.
You want to delete your remote tracking branch, and you want to leave the corresponding branch untouched on the remote.
You delete your remote tracking branch and you want to delete the corresponding branch on the remote, too.
For all of these, it’s good to have your working tree clean, of course.
The first one is pretty easy. Let’s tell Git to delete all our remote tracking branches that no longer exist on the origin remote:
$ git fetch --pruneor if you want to specify a remote:
$ git fetch --prune someremoteor if you want to prune all remotes:
$ git fetch --prune --allIn this case, you have a remote tracking branch on your clone that you want to delete. But you don’t want to delete that branch from the server.
Use -d for delete and -r for remote:
$ git branch -dr remote/branchFor example:
$ git branch -dr origin/topic99Finally, let’s say you’ve deleted your remote tracking branch on your clone, as per above, and you also want to delete it on the remote.
We’re going to (perhaps surprisingly) use git push for this.
To delete a branch on the remote, you:
$ git push someremote --delete branchnameFor example:
$ git push origin --delete topic99And that’s it! Make sure you delete your remote tracking branch if you haven’t done so already.
It’s possible that you might have multiple remotes. (This is commonly put in place when you’ve forked someone’s repo on GitHub.)
How do remote tracking branches work in that case?
Let’s say your main remote is called origin as usual. But you’ve also set another remote called remote2, unoriginally.
Someone else pushes a new branch called foobranch (slightly more originally) up to remote2 and you’d like to get it.
So you do this:
$ git fetch remote2
remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 12 (delta 6), reused 11 (delta 5), pack-reused 0
Unpacking objects: 100% (12/12), 1.61 KiB | 34.00 KiB/s, done.
From github.com:user/somerepo
* [new branch] foobranch -> remote2/foobranchSo far so good. Let’s switch to it:
$ git switch foobranch
branch 'foobranch' set up to track 'remote2/foobranch'.
Switched to a new branch 'foobranch'Wait! We’re tracking at remote2? That’s a little weird because it’s the other person’s repo. Maybe you have permission to write to it, and that’s what you want to do. But it’s more probably you’d like your own version of this branch on your repo as well.
You can do that by pushing it to your remote with -u again.
$ git push -u origin foobranchAnd that’ll do it.
If you look at your branches with git branch -avv you’ll see now several foobranch variants for different clones.
foobranch
remotes/origin/foobranch
remotes/remote2/foobranchIf you want to keep your origin/foobranch in sync with that on remote2, you’ll have to do a bunch of merging.
$ git fetch remote2 # Get remote2 changes
$ git switch foobranch # Get onto the merge-into branch
$ git merge remote2/foobranch # Merge changes from remote2
$ git push origin foobranch # Push changes back to origin(You can leave the origin foobranch off the push if you’ve already pushed it with -u earlier, of course.)
At that point, every foobranch should be on the same commit.