Don't be afraid of branches! Branches—branch names in particular, because the word "branch" is ambiguous in Git—are so cheap as to be nearly free. For Git, creating a new branch consists of writing one commit hash ID into one file,1 and removing a branch consists of removing that one file.
What's less cheap, though still not that expensive, are the commits themselves. Every commit has exactly one "true name", which is that big ugly hash ID, face0ff...
or cafedad...
or whatever. Each of those commits records its parent commit ID, so you (and Git) can work backwards, starting from the last commit on a branch, to find all the commits on the branch. These commits are permanent and unchanging. What can change is not the commits, but the branch names.
What you drew has three commits, but you didn't draw their explicit relationship to each other. That relationship, which is a sort of parent/child thing, is recorded in each commit by recording just the parents.2 So assuming (reasonably, I think) that commit A
came first, then B
, then C
, what you have is this:
A <- B <- C <-- branch
That is, the branch name, branch
, points to your commit C
, and your commit C
points back to B
("B is my parent", says C) and B
points back to A
. None of these can parent pointers can change, but branch names can change, and making new branch names is so cheap it might as well be free.
So let's make a new name and point it to C
:
$ git checkout -b temp
Now we have:
A--B--C <-- branch, HEAD -> temp
I put in the HEAD ->
here to denote that the current branch is temp
, even though there are two branch names both pointing to commit C
.
Now what you can do is copy C
to a new commit that's mostly like C
, but not quite the same. We'll call the copy C'
. In C'
, we want these two key differences:
- Our new parent should be
A
, not B
- We should figure out what we changed going from
B
to C
, and apply those changes as a patch onto A
, instead of onto B
.
The command that copies commits like this, then moves the branch name, is git rebase
:
$ git rebase --onto HEAD~2 HEAD~1 # we'll explain these later
The rebase command copies each of the "rebased" commits, which in this case is just commit C
. It then moves the current branch name to point to the copies:
A--B--C <-- branch
\
`---C' <-- HEAD -> temp
Note that the original commit C
is still in there, and it's still at the tip of branch branch
. We abandoned it from temp
when we made the copy, but branch
still remembers it.
Now you can test your change in isolation: does C'
work correctly without B
? If so, any problems you get in C
are probably introduced by B
, since C'
vs A
is pretty much "the same" as C
vs B
.
1This is an implementation detail, and branch names can also be stored with many-in-one-file instead of one-per-file, but it should give you an idea of just how cheap branches really are.
2This is necessary because at the time you create a commit, it has no children. Once you've created the commit, it is permanent and unchanging, so it can't record its children. Its children don't exist yet! As soon as one child comes into existence, though, that child has its parent, and that child's parent is also permanent and unchanging, so the child can and does record its parent.
Explanation of HEAD~2
and HEAD~1
Whenever you want to identify a particular commit in Git, you can give its raw SHA-1 hash. You can cut-and-paste these from git log
output, for instance. But you can also use a symbolic name, such as a branch or tag name, or the special name HEAD
. These symbolic names simply resolve to a commit ID:
$ git rev-parse HEAD
de2efebf7ce2b308ea77d8b06f971e935238cd2f
You can abbreviate these things, so that face0ff
or cafebabe
works if the actual ID starts with that.
You can also use a relative name, which is made by taking any suitable name and adding ~number
. So both HEAD~2
and de2efeb~2
mean the same thing here. The ~
suffix means "go back some number of parents", and the number of parents to go back is whatever comes after the ~
.
Since HEAD
is always the current commit, HEAD~1
means "go back one parent", and HEAD~2
means "go back two parents". From C
, going back one parent gets you B
, and going back two gets you A
.
Thus:
--onto HEAD~2
means "copy so that we add after A
", while:
HEAD~1
in this case means "don't copy commit B
or earlier"—which leaves just commit C
.