Normally you think of writing code as a linear sequence of changes. You start with an empty file, add some things, test them, add some more, test some more, and eventually the code is complete.
In Git we might think of this as a sequence of commits. Let’s look at a graph (Figure 5.1) where I’ve numbered commits 1-5. There, (1)
was the first commit we made on the repo, (2)
is some changes we made on top of (1)
, and (3)
is some changes we made on top of (2)
, etc.
Git always keeps track of the parent commit for any particular commit, e.g. it knows the parent commit of (3)
is (2)
in the above graph. In this graph, the parent relationship is indicated by an arrow. “The parent of commit 3 is commit 2”, etc. It’s a little confusing because clearly commit 3 came after commit 2 in terms of time, but the arrow points to the parent, which is the opposite of the nodes’ temporal relationship.
A branch is like a name tag stuck on one specific commit. You can move the name tag around with various Git operations.
We’re assuming the default branch is called main
. If you haven’t done so already, configure your default branch as shown in the Git Basics chapter.
So to make it a little more complete, we can show that branch in Figure 5.2. There’s our main
branch attached to the commit labeled (5)
.
It’s tempting to think of the whole sequence of commits as “the branch”, but this author recommends against it. Better to keep in mind that the branch is just a name tag for a single commit, and that we can move that name tag around.
But Git offers something more powerful, allowing you (or collaborators) to pursue multiple branches simultaneously.
So there might be multiple collaborators working on the project at the same time.
And then, when you’re ready, you can merge those branches back together. In Figure 5.4, we’ve merged commit 6 and 7 into a new commit, commit 9. Commit 9 contains the changes of both commits 7 and 6.
In that case, somebranch
and anotherbranch
both point to the same commit. There’s no problem with this.
I’m actually oversimplifying this a little. When you merge a branch into another, really only the branch you’re merging into moves, not both of them. So to get the two branches to point to the same commit, you’d have to do two merges:
somebranch
intoanotherbranch
and thenanotherbranch
intosomebranch
. (Or the other way around.) And then they’d point to the same commit.
And then we can keep merging if we want, until all the branches are pointing at the same commit (Figure 5.5).
And maybe after all this we decide to delete somebranch
and anotherbranch
; we can do this safely because they’re fully merged, and can do this without affecting main
or any commits (Figure 5.6).
This chapter is all about getting good with branching and partially good with merging.
If you like interactive tutorials, Peter Cottle has put together a great website called Learn Git Branching18. I highly recommend it before, during, and/or after reading this chapter.
git pull
When you do a pull, it actually does two things: (a) fetch all the changes from the remote repo and (b) merge those changes.
If two or more people are committing to the same branch, eventually git pull
is going to have to merge. And it turns out there are a few ways it can do this.
For now, we’re going to tell git pull
to always classically merge divergent branches, and you can do that with this one-time command:
$ git config set --global pull.rebase false
If you don’t do that, Git will pop up an error message complaining about it the first time it has to merge on a pull. And you’ll have to do it then. (Leave the word set
out of that command if it fails on older Gits.)
When we talk about rebasing later, this will make more sense.
HEAD
and BranchesWe said earlier that HEAD
refers to a specific commit, namely the commit you’re looking at right now in your unmodified working tree.
And we also said that was a bit of a lie.
In normal usage, HEAD
points to a branch, not to a commit. Only in detached head state does HEAD
point directly to a commit (i.e. when it’s detached from all branches).
If we look at Figure 5.7, we see HEAD
is pointing to a branch as per normal.
But if we check out an earlier commit that doesn’t have a branch, we end up in detached head state, and it looks like Figure 5.8.
So far, we’ve been making commits on the main
branch without really even thinking about branching. Recalling that the main
branch is just a label for a specific commit, how does the main
branch know to “follow” our HEAD
from commit to commit?
It does it like this: the branch that HEAD
points to follows the current commit. That is, when you make a commit, the branch HEAD
points to moves along to that next commit.
If we were here back at Figure 5.7, when HEAD
was pointing to the main
branch, we could make one more commit and get us to Figure 5.9.
Contrast that to detached head state, back in Figure 5.8. If we were there, a new commit would get us to Figure 5.10, leaving main
alone.
At this point, there’s nothing stopping you from creating a new branch at the same commit as HEAD
, if you want to do that. Or maybe you are just messing around and decide to switch back to main
later, abandoning the commits you’ve made in detached HEAD
state.
Now that we have the abstract theory stuff laid out, let’s talk specifics.
Before we start, let’s see how to list branches.
$ git branch * main
This is telling you there’s one branch, and you have it checked out (the *
lets you know that).
If I make a new branch called foobranch
and switch to that, I’ll see this:
% git branch
* foobranch main
If I then detach the HEAD
, I end up here:
% git branch
* (HEAD detached at 10b6242)
foobranch main
But you can always see what branch you’re on with git branch
or git status
.
When you make the first commit to a new repo, the main
branch is automatically created for you at that commit.
But what about new branches we want to make?
Why make a branch? A common case is that you want to work on your own commits without impacting the work of others. (In this case you’re really just putting off the work until you merge your branch with theirs, but it’s a good workflow.)
Another case is that you want to mess around with some changes but you’re not sure if they’ll work. If they end up not working, you can just delete the branch. If they do work, you can merge your changes back into the non-messing-around branch.
The most common way to make new branches is this:
Switch to the commit or branch from which you want to make the new branch.
Make the new branch there and switch HEAD
to point to the new branch.
Let’s try it. Let’s branch off main
.
You might already have main
checked out (i.e. HEAD
points to main
), but let’s do it again to be safe, and then we’ll create a branch with git switch
:
$ git switch main $ git switch -c newbranch
Normally you can just switch to another branch (i.e. have HEAD
point to that branch) with git switch branchname
. But if the branch doesn’t exist, you need to use the -c
switch to create the branch before switching to it.
Make sure all your local changes are committed before switching branches! If you
git status
it should say “working tree clean” before you switch. Later we’ll learn about another option when we talk about stashing.
So after checking out main
, we have Figure 5.11.
And then with git switch -c newbranch
, we create and switch to newbranch
, and that gets us to Figure 5.12.
That’s not super exciting, since we’re still looking at the same commit, but let’s see what happens when we make some new commits on this new branch.
The branches we’re making here exist only on your local clone; they’re not automagically propagated back to wherever you cloned the repo from.
The upshot is that if you accidentally (or deliberately) delete your local repo, when you
git clone
again, all your local branches will be gone (along with any commits that aren’t part ofmain
or any other branches pushed to the server).There is a way to set up that connection where your local branches are uploaded when you push, called remote-tracking branches.
main
is connected to a remote tracking branch (usually calledorigin/main
) which is whygit push
frommain
works whilegit push
fromnewbranch
, which is not by default connected to a remote tracking branch, gives an error. But we’ll talk about all this later.
This is not really that different than what we were doing with our commits before. Before we made a branch, we had HEAD
pointing to branch main
, and we were making commits on main
.
Now we have HEAD
pointing to newbranch
and our commits will go there, instead.
Right after creating newbranch
, we had the situation in Figure 5.12. Now let’s edit something in the working tree and make a new commit. With that, we’ll have the scenario in Figure 5.13.
Right? Let’s make another commit and get to Figure 5.14.
We can see that newbranch
and main
are pointing at different commits.
If we wanted to see the state of the repo from
main
’s perspective, what would we have to do? We’d have togit switch main
to look at that branch.
Now for another question. Let’s say we’ve decided that we’re happy with the changes on newbranch
, and we want to merge them into the code in the main
branch. How would we do that?
Bringing two branches back into sync is called merging.
The branch you’re on is the branch you’re bringing other changes into. That is, if you’re on Branch A, and you tell git to “merge Branch B”, Branch B’s changes will be applied onto Branch A. (Branch B remains unchanged in this scenario.)
But in this section we’re going to be talking about a specific kind of merge: the fast-forward. This occurs when the branch you’re merging from is a direct descendant of the branch you’re merging into.
Let’s say we have newbranch
checked out, like from the previous example in Figure 5.14.
I decide I want to merge main
’s changes into newbranch
, so (again, with newbranch
currently checked out):
$ git merge main Already up to date.
Nothing happened? What’s that mean? Well, if we look at the commit graph, above, all of main
’s changes are already in newbranch
, since newbranch
is a direct ancestor.
Git is saying, “Hey, you already have all the commits up to main
in your branch, so there’s nothing for me to do.”
But let’s reverse it. Let’s check out main
and then merge newbranch
into it.
$ git switch main
Now we’ve moved HEAD
to track main
, as shown in Figure 5.15.
And newbranch
is not a direct ancestor of main
(it’s a descendant). So newbranch
’s changes are not yet in main
.
So let’s merge them in and see what happens (your output may vary depending on what files are included in the merge):
$ git merge newbranch
Updating 087a53d..cef68a8
Fast-forward
foo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-)
And now we’re at Figure 5.16.
Wait a second—didn’t we say to merge newbranch
into main
, like take those changes and fold them into the main
branch? Why did main
move, then?
We did say that! But let’s stop and think about how this can happen in the special case where the branch you’re merging into is a direct ancestor of the branch you’re merging from.
It used to be that main
didn’t have commits (5)
or (6)
in the graph, above. But newbranch
has already done the work of adding (5)
and (6)
!
The easiest way to get those commits “into” main
is to simply fast-forward main
up to newbranch
’s commit!
Again, this only works when the branch you’re merging into is a direct ancestor of the branch you’re merging from.
That said, you certainly can merge branches that are not directly related like that, e.g. branches that share a common ancestor but have both since diverged.
Git will automatically fast-forward if it can. Otherwise it does a “real” merge. And while fast-forward merges can never lead to merge conflicts, regular merges certainly can.
But that’s another story well get into in the Merging and Conflicts chapter.
If you’re done merging your branch, it’s easy to delete it. Importantly, this doesn’t delete any commits; it just deletes the branch “label” so you can’t use it any longer. You can still use all the commits.
Let’s say we’ve finished the work on our topic1
branch and we want to merge it into main
. No problem:
$ git commit -m "finished with topic1" # on topic1 branch
$ git switch main $ git merge topic1 # merge topic1 into main
At this point, assuming a completed merge, we can delete the topic1
branch:
$ git branch -d topic1 Deleted branch topic1 (was 3be2ad2).
Done!
A topic branch is what we call a local branch made for a single topic like a feature, bug fix, etc. In this guide I’ll name branches literally
topic
to indicate that it’s just an arbitrary branch. But in real life you’d name the topic branch after what it is you’re doing, likebugfix37
,newfeature
,experiment
, etc.
But what if you were working on a branch and wanted to abandon it before you merge it into something? For that, we have the more imperative Capital D
option, which means, “I really mean it. Delete this unmerged branch!”
$ git branch -D topic1
Use lowercase -d
unless you have reason to do otherwise. It’ll at least tell you if you’re about to lose your reference to your unmerged commits, and then you can override with -D
if you really want to.