git: how to move a branch's root two commits b

2019-02-15 16:14发布

问题:

Let's say I have:

A - B - C - D - E - F  master
            \ 
             \- G - H  new feature branch

Now I realize that commits B and C actually belong to the new feature, so I want to move them to the "new feature branch". In other words, I want the "new feature branch" to start at A, and include commits B and C:

A - D - E - F  master
 \ 
  \- B - C - G - H  new feature branch

How do I do this? From what I've read, it seems that rebase is the feature I'm looking for, but I'd like to be sure before I mess up my repository.

(I've searched and found lots of questions and examples that are very similar, but none that are exactly like the scenario I've described, so I'm asking to be sure (a repo is a precious thing to ruin, after all)).

回答1:

To fix master:

git checkout A
git cherry-pick master~3..master # apply the changes we actually want to keep on master
git branch -f master # reposition master on new position
git checkout master

To drop commit D from the feature branch:

git checkout feature-branch~3 # or git checkout C
git cherry-pick feature-branch~2..feature-branch # apply the last 2 revisions
git branch -f feature-branch
git checkout feature-branch


回答2:

Edmundo's answer is right (and upvoted), but it's worth pointing out a few extra items.

First, your question talks about "moving the branch's root"—but this is Git; branches don't have roots, not the way you are thinking anyway. Let's look at your graph drawing:

A - B - C - D - E - F  master
            \ 
             \- G - H  new feature branch

I think that you, like me when I first started using Git, would like to think of commits A-B-C-D-E-F as being on the master branch, and commits G-H as being on the feature branch. But that's not how Git works. Let's re-draw this, without changing any of the commit links:

           E--F   <-- master
          /
A--B--C--D
          \
           G--H   <-- feature

This should make it clearer that, as far as Git is concerned, commits A-B-C-D are on both branches. There is only one root. That's commit A: it's a root because it has no parent commit, no backwards link in the chain from commit to parent.

Second, no matter how you go about this, you wind up having to copy some commits. The reason is that each commit's parent ID is part of that commit's identity. The "true name" of any commit is its hash ID, and the hash ID is built by reading the complete contents of the commit: source tree, commit message, author name and date, etc., but always including the parent ID too.1 You want your final graph to resemble:

  D--E--F   <-- master
 /
A
 \ 
  B--C--G--H   <-- feature

but the existing D links (or points) back to the existing C, not to A, and the existing G points back to the existing D.

This is why cherry-pick works: git cherry-pick essentially copies a commit. The new copy "does the same thing" as the original, but has something different, even if it's as simple as "my parent is ...". (Usually it also has a different attached tree object as well, it's just that the change it makes, when compared to its new parent, is the same as the change the original makes when the original is compared to the original's parent). What this means is that you can't actually get what you want, just what you need:

  D'-E'-F'  <-- master
 /
A
 \ 
  B--C--G'-H'  <-- feature

where the little tick mark indicates that the result is a copy of the original.

What happens to the originals? The answer is: they're still there, in the repository, in case you need them. The full picture is more like this:

    D'-E'-F'   <-- master
   /
  /        E--F   [abandoned]
 /        /
A--B--C--D
       \  \
        \  G--H   [abandoned]
         \
          G'-H'   <-- feature

While git cherry-pick works, what git rebase—especially git rebase -i—does is to do these copies in a fancy automated fashion, with a final step to move the branch name, abandoning the original commits. So git rebase -i is sometimes an easier way to go about it.

If you run git rebase -i you see all those pick commands, and those literally run git cherry-pick: it really is doing a series of cherry-picks. If you do these yourself, it may be clearer what is going on, and you have a lot more control over when the branch labels are moved-around.


1For merge commits, this is parents, plural. All parents participate in the hash.



回答3:

You can do this with git rebase.

First, let's rebase some commits from master:

$ git checkout master
$ git rebase --onto A C

This will move all commits in the range from C to master (but not including C itself) onto the commit A.

Now rebase feature but throw out commit D:

$ git checkout feature
$ git rebase --onto C D

Similar to the previous command, this will move commits in the range from D to feature (not including D itself) onto C.



标签: git rebase