Large edit: you probably want rebase.
I'm putting the rebase section at the top, but you may want to read the rest first.
Rebasing
One of the most useful things in git is to cherry-pick an entire "branchlet" (a string of commits ending in a branch tip) to, in effect, move them. That is, assuming you are now comfortable with cherry-picking (see below if needed), let's say you have something like this in your commit graph:
o--o <-- fix-bug-1234
/ \
...--o--o--o---o--o--o <-- develop
\
o--o--o--o <-- feature
You started work on feature
a while ago, then had to make some emergency changes on develop
and an emergency bug fix that is merged back in now; and now it is time to get back to working on feature
.
It sure would be nice, though, to work on a version of feature
that starts from the latest good stuff in develop
.
So let's do just that. We start by checking out a new feature branch based on the tip of develop
:
git checkout -b new-feature develop
giving us this:
o--o <-- fix-bug-1234
/ \
...--o--o--o---o--o--o <-- develop, HEAD->new-feature
\
o--o--o--o <-- feature
Now we simply cherry-pick in all four feature
commits. We could write feature~4..feature
but that requires that we count up the commits, so since we know all about git set notation,1 we use develop..feature
to find that very same set of commits. This allows us to use as our second git command:
git cherry-pick develop..feature
which copies those four commits:
o--o <-- fix-bug-1234
/ \
...--o--o--o---o--o--o <-- develop
\ \
\ o--o--o--o <-- HEAD->new-feature
\
o--o--o--o <-- feature
Now we rename feature
to old-feature
and then rename new-feature
to feature
:
git branch -m feature old-feature
git branch -m new-feature feature
Or we do all of this the easy way, using git rebase
. We don't have to start with a git checkout -b
, then a git cherry-pick
and two rename steps, we just do one git checkout
followed by one git rebase
:
git checkout feature
git rebase develop
The rebase
command uses our one argument, develop
, to:
- figure out which commits to cherry-pick:
develop..feature
- see where to copy them: after the tip of
develop
There are fancier forms of rebase
that let us separate the list-of-commits-to-cherry-pick from the place-they-go-after, but for the typical case, we do not need these.
Note that when we do a rebase, the rebase command normally omits merge commits from the copying, but copies commits "behind" the merge commits unless they are excluded by the set notation (the develop..feature
stuff). When there are merge commits in the sequence this can produce surprising results.
In addition, since rebasing copies commits—it really is a series of cherry pick operations—you are only 100% safe to do this if you are the only person with the originals of those commits. If someone sharing your work (via fetch or push) has the originals as well, they may be using them and they may re-introduce those originals alongside your rebased copies. If you are the only one with the originals, this is not a problem. If those others who do have copies know all about your rebased copies, they can deal with the issue themselves (although they may not want to).
1If you don't know about this set notation, you are missing something that makes life much easier in git.
You are missing the point of a merge—or perhaps it would be more accurate to say that the point of a merge is missing what you would like done.
Let's take a look at the point of merge in the first place.
Suppose you and Alice are working on two different things. You are supposed to get widgets to slice both left and right, rather than slicing rightwards only. Alice is supposed to make crossbars hold up better.
Both of you start from a common base, where widgets only slice rightwards, and crossbars break.
This week you managed to make the widgets slice leftwards, and Alice fixed crossbars. So you (or Alice) now want to combine your work and her work:
$ git checkout larry-widget-fixes
$ git merge alice-crossbar-fixes
If this made all of your files match Alice's files exactly, that would undo all your work from this week.
Therefore, git merge
does not make things match up. Instead, it goes back to the common base to see (1) what you changed, and (2) what Alice changed. It then tries to combine these two sets of changes into one combined (merged) change.
In this case, it does so by adding her changes to yours (because we merged her work into yours, rather than your work into hers).
Note that the merge base—the common starting point—is one of the key items here.
What if you don't really want a merge?
Let's say you don't want to combine some changes. You want instead to make some file(s) match.
There are a lot of ways to do this, depending on just what results you want. Let's look at three of these ways.
Just make one file match, by extracting the other version.
Let's say that while fixing widgets, you accidentally broke shims. Alice's branch has a good copy of shims.data
(as does the merge base, for that matter, but let's just use Alice's here):
git checkout alice-crossbar-fixes -- shims.data
This extracts the version of file shims.data
from the tip of Alice's branch, and now you have the one you want.
Just un-do one commit, by commit ID.
Let's say that you realize you broke the shims in commit badf00d
, and all the other changes you made there should be undone too, or it's OK to undo them. You can then run:
git revert badf00d
The git revert
command turns the given commit into a patch, then reverse-applies the patch so that the effect of that commit is un-done, and makes a new commit out of the result.
You want to give up on everything entirely. You realize that everything you did this week, while it makes widgets slice leftwards as well as rightwards, is a mistake because widgets should not slice rightwards at all. Here you can use git reset
.
I won't give an example since this (a) this is probably not what you want; (b) git reset
is overly complicated (the git folks stuffed about five to eight logically-different operations into one command); and (c) there are plenty of good examples on StackOverflow already. (This is true for git revert
in step 2 as well, but git revert
is a lot simpler so I left it in.)
What if you have made a bad merge?
This is more complicated.
Remember that when we did the merge in the first example, we had a common starting point, at the beginning of the week before you and Alice started different tasks:
...--o--*--o--o--o <-- larry-widget-fixes
\
o-o-o-o-o <-- alice-crossbar-fixes
The common merge base is commit *
.
Once you finish your merge, though, we have this (I labeled the merge commit M1
to make it easy to see):
...--o--o--o--o--o--M1 <-- larry-widget-fixes
\ /
o-o-o-o-o <-- alice-crossbar-fixes
Now suppose Alice makes some more fixes, and let's have you make one more change too:
...--o--o--o--o--o--M1-o <-- larry-widget-fixes
\ /
o-o-o-o-*--o--o <-- alice-crossbar-fixes
Now you go to pick these up by merging again:
git merge alice-crossbar-fixes
and git finds the common merge base. This is the nearest commit that is on both branches, which I marked with *
again. Git will find out what Alice changed since *
, and what you changed since *
, and try to combine those changes. You already picked up all of Alice's earlier changes—they are in your branch already, in M1
—so it is a good thing that git does not look at those again. Git will just try to add Alice's new changes since *
to your changes since *
(your version of Alice's changes).
But what if you got them wrong? What if your version of Alice's changes, in M1
, are a bunch of wrong files?
At this point you must choose whether to retry the merge, or to use git's many other tools to fix things up.
Retrying a merge
To retry the merge, you must back up to a point before the merge happened. For instance, suppose we draw a new arrow to the commit just before M1
, like so:
..........<-- retry
.
...--o--o--o--o--o--M1-o <-- larry-widget-fixes
\ /
o-o-o-o-o--o--o <-- alice-crossbar-fixes
(imagine it's an arrow rather than just some crappy ASCII dots, anyway). We can leave the existing branch labels in there, pointing where they do, we just add this new retry
, by doing:
git branch retry <commit-id> && git checkout retry
or:
git checkout -b retry <commit-id>
(these both essentially do the same thing, giving us retry
pointing to the desired commit). Now if we do:
git merge alice-crossbar-fixes
git will try to merge the commit-before-M1
that retry
points to, with the (new, current) tip of alice-crossbar-fixes
. Git will once again find a merge base, which is the nearest commit on both of these branches, which is our original merge base again.
In other words, we will re-do the merge, ignoring the bad merge M1
, but also ignoring new work done since then.
Cherry picking
We can git checkout -b <commit>
on any particular commit to get back to that state, and then start doing things like git cherry-pick
to re-add specific commits.
Let's say we like the commit-before-M1
a lot (it looks really close), so we make our new branch—let's call it something other than retry
—that points here:
git checkout -b rework <commit-ID>
Now we can pick and choose from other commits, extracting just the changes they made, using git cherry-pick
. We give it more commit IDs, more of those 197a3c2...
strings, and it compares the state just before that commit to the state at that commit. The result is a patch, but in fact, it's somewhat better than a regular diff-only patch, for two reasons:
- it has a commit message attached, which git can re-use, and
- it has a particular place in the commit graph, so git can (if necessary) find a merge base and do a 3-way merge, when applying it! Normally this is not a big deal but it's pretty handy for some cases.
Leaving out the second advantage and just thinking about it as a patch, what git cherry-pick
does is to apply the patch to the current commit, then make a new commit, re-using the original commit message.
If you would like to re-apply many commits this way, you can give git cherry-pick
multiple commits and it will do them all:
git cherry-pick xbranch~3..xbranch
This applies every commit that is on xbranch
except for commits starting at xbranch~3
and earlier, i.e., we count back three steps along xbranch
(assuming no merges...) and apply the remaining three commits:
N N Y Y Y [do we take it?]
| | | | |
v v v v v
...-o-o--o-o-o <-- xbranch
...-o--o <-- larry-widget-fixes (before cherry pick)
(and see rebasing, at the top)