Raman
Raman

Reputation: 19685

Transplanting history onto the *current* branch

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

Answers (2)

Raman
Raman

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

torek
torek

Reputation: 490068

TL;DR

Use git cherry-pick, and (later), git reset or git branch -f as appropriate.

Long

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.

How rebase works internally

The way rebase actually achieves this is pretty simple:

  1. It remembers the current branch (or for a detached HEAD, hash ID): saved_branch=$(git symbolic-ref -q --short HEAD), more or less.

  2. 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.

  3. It runs git checkout --detach $onto (or something more or less equivalent).

  4. It runs git cherry-pick <saved list>, i.e., git cherry-pick $stop..$saved_branch.

  5. 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.)

What you want

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

Related Questions