Reputation: 19685
I know how to use git rebase --onto
to transplant history. However, I often find myself wanting to transplant history from a different branch onto my current branch, without the intermediate detached HEAD state.
In other words:
git checkout foo
git rebase --onto foo A^ B
# results in a detached HEAD
# how to avoid this last step?
git checkout -B foo
Upvotes: 1
Views: 114
Reputation: 19685
@torek's answer is excellent, but I wanted to post the TL;DR; to the question I specifically asked, which is:
git cherry-pick A..B
The key information from @torek's answer being, of course, that cherry-pick
accepts a range.
Upvotes: 0
Reputation: 490068
Use git cherry-pick
, and (later), git reset
or git branch -f
as appropriate.
Bear with me for a bit while I review things you already know, since they will make the answer much clearer.
Rebase works by copying commits. That is, given a commit graph of the form:
...--o--*--I <-- mainline
\
F--G--H <-- branch (HEAD)
we often find ourselves wanting to have the commit sequence F-G-H
come after commit I
on the main line. To do that, we run git checkout branch && git rebase mainline
, which copies commits F-G-H
to new and improved ones. Then, having made the copies, it moves the current branch name to point to the last such copy:
F'-G'-H' <-- branch (HEAD)
/
...--o--*--I <-- mainline
\
F--G--H [abandoned, sort of]
As you already know, we can separate the set-of-commits-to-be-copied from the point-at-which-they-should-land using git rebase --onto
. For instance, instead of copying all of F-G-H
, we might like to leave F
behind, copying only G
and H
:
G'-H' <-- branch (HEAD)
/
...--o--*--I <-- mainline
\
F <-- ???
\
G--H [abandoned, sort of]
When we do this, it's a good idea to attach some name to the commit(s) we are leaving behind before we have Git move the name branch
. We can do that using git branch
, for instance, or git checkout -B
, or whatever. The key, however, is that we use --onto
to select commit I
—the target commit after which the copies will go—and the other argument to limit which commits get copied. The commits that are copied are those after the argument we give, up to and including the one on the HEAD
branch.
The arguments for this --onto
case look like:
git rebase --onto $target $stop
where $stop
is, e.g., commit F
.
The way rebase actually achieves this is pretty simple:
It remembers the current branch (or for a detached HEAD, hash ID): saved_branch=$(git symbolic-ref -q --short HEAD)
, more or less.
It lists the hash IDs of the commits to be copied. It uses git rev-list
with a lot of fancy options for this, to handle all the hard cases, but for our easy cases, we can just use $stop..HEAD
.
It runs git checkout --detach $onto
(or something more or less equivalent).
It runs git cherry-pick <saved list>
, i.e., git cherry-pick $stop..$saved_branch
.
Once all the cherry picks are complete—including any stops to resolve conflicts, handled by the human—it runs git branch -f $saved_branch HEAD
(or something more or less equivalent) to make the saved branch name point to the last copied commit. (There's also a bit of mucking-about with the special name ORIG_HEAD
.)
In your case, you are already at the --onto
part. That is, step 1 is irrelevant, and step 2 cannot happen quite the same way. Step 3 should not happen at all—we want the new commits to grow the current branch!
That leaves us with just steps 4 and 5 to achieve.
Step 4 is particularly easy in any modern Git, where git cherry-pick
can handle a sequence: we just run:
git cherry-pick $stop..$branch
where $stop
is the stop-at commit (the commit we don't want copied), and $branch
is the branch that points to the last commit that we do want copied. If we are on mainline
now, that's git cherry-pick <hash-of-F>..branch
or git cherry-pick branch~2..branch
, to copy commits G
and H
.
To achieve step 5—to make $branch
point back to commit F
—we will leave this for later, after our copies are done. When those copies are done, we can either run:
git checkout $branch && git reset --hard $stop
or:
git branch -f $branch $stop
while we're on mainline
.
Upvotes: 5