I have a tree:
/--b1---b2 <-- topic b
/
a1---a2 <-- topic a
where 'b' depends on 'a'. Then I realize I need to do some changes related to topic '开发者_StackOverflow中文版a' in order to continue with 'b', but i'd like to do them on 'b' as a normal development course of 'b':
/--b1---b2---a3---a4---b3---b4---a5---b5 <-- topic b
/
a1---a2 <-- topic a
Then, when the thing I want to accomplish on 'b' is done, I'd like my tree to look like this:
/--b1---b2--------m---b3'---b4'---b5' <-- topic b
/ /
a1---a2---a3'---a4'---a5' <-- topic a
as if I actually did all the changes on 'a', then merged them on 'b' and then continued on 'b'.
I know I can do this manually doing:
1- rebase/cherry-pick 'a' commits from branch 'b' to 'a'
2- create a temporal branch 'b-tmp' on 'b'. 3- reset branch 'b' to 'b2'. 4- merge 'a' onto 'b'. 5- rebase/cherry-pick 'b' commits from 'b-tmp' onto 'b'. 6- delete branch 'b-tmp'.I can create some script to do this, I'd just like to know if there are better ways/ideas to do this, other than this 6 steps.
Let me start out with saying that I'd rather integrate the topic branches often (Integrate Early, Build Often ... rings a bell?). So in fact, when there are changes that should go onto a eventually, I'd go back, do the changes on a, and rebase b on top of a.
If a needs to be stable for some other purpose, I'd make a a-for-b branch to contain the work temporarily, and rebase b on top of that.
If you really must work with 'topic-a' changes inside branch 'b', Here's what i'd do.
Note: screencasts included
Getting to the starting point
This is a simple working tree with the layout from the question:
mkdir /tmp/q
cd /tmp/q
git init .
touch f
git add f
git commit -am initial
git checkout -b a; for a in a{1,2}; do echo $a>$a; git add $a; git commit -am $a; git tag $a; done
git checkout -b b; for a in b{1,2} a{3,4} b{3,4} a5 b5; do echo $a>$a; git add $a; git commit -am $a; git tag $a; done
git show-branch
See screencast here
Reorganizing commits onto their topic branches
git checkout -b a_new a # for safety, work on a clone of branch a
git reset --hard b # start by moving it to the end of the b branch
git status # (just show where we are)
git log --oneline
git rebase -i a # rebase it on top of the original branch a
# not shown: delete every line with non-branch-a commits (b1-b5)
# see screencast
git log --oneline # (just show where we are again)
git checkout -b b_new b # for safety, work on a clone of branch b
git log --oneline # (just show where we are again: the end of branch b)
git rebase -i a_new # this time, rebase it on top of the new branch a_new
git log --oneline # (check results)
git show-branch b_new a_new
Again, see screencast
Inspecting results
We can now do a before/after tree comparison:
sehe@natty:/tmp/q$ git show-branch a b
! [a] a2
! [b] b5
--
+ [b] b5
+ [b^] a5
+ [b~2] b4
+ [b~3] b3
+ [b~4] a4
+ [b~5] a3
+ [b~6] b2
+ [b~7] b1
++ [a] a2
sehe@natty:/tmp/q$ git show-branch a_new b_new
! [a_new] a5
* [b_new] b5
--
* [b_new] b5
* [b_new^] b4
* [b_new~2] b3
* [b_new~3] b2
* [b_new~4] b1
+* [a_new] a5
Making it permanent:
for name in a b;
do
git branch -m $name ${name}_old &&
git branch -m ${name}_new $name
done
git branch -D a_old b_old # when sure
Notes
I have deliberately chosen to make non-conflicting changes for the demo. Of course in real life you'll get merge conflicts. Use git mergetool
and git rebase --continue
.
If your changes are hygienic and really belong on their respective topic branches, chances are that conflicts should be few and easily resolved. Otherwise, it is time to revise your branching scheme (see Martin Fowler e.a.)
Discussion
In response to
You also say that "when there are changes that should go onto 'a' eventually, I'd go back, do the changes on 'a', and rebase 'b' on top of 'a'". I'm not sure I understand this also. Could you explain?
I mean that I would try to make the a3,a4,a5 revisions on the a branch anyway, and rebase b onto that, so
- the person doing the merge is the same person doing the commits
- you do the commit and merge in succession (meaning you don't mess things up due to short-term-memory loss)
<-- this is the
Merge Early/Soon/Frequently
mantra
- you avoid unnecessary merge conflicts
- you don't have to 'go back in time' later and rewrite the original commits: a3, a4, a5 would just be merged, not copied (see Git Cherry-pick vs Merge Workflow)
Here is the 'Getting to the starting' point, but adapted to my idealized workflow. Note that the end-result of this alternative result is already exactly what you end up after all the juggling shown under 'Reorganizing commits onto their topic branches' above!
mkdir /tmp/q
cd /tmp/q
git init .
touch f
git add f
git commit -am initial
git checkout -b a; # no change
echo a1>a1; git add a1; git commit -am a1
echo a2>a2; git add a2; git commit -am a2
git checkout -b b; # start off the a branch
echo b1>b1; git add b1; git commit -am b1
echo b2>b2; git add b2; git commit -am b2
git checkout a # go back to the a branch for a3 and a4
echo a3>a3; git add a3; git commit -am a3
echo a4>a4; git add a4; git commit -am a4
git checkout b
git rebase a # here is the magic: rebase early
echo b3>b3; git add b3; git commit -am b3
echo b4>b4; git add b4; git commit -am b4
git checkout a # go back to the a branch for a5
echo a5>a5; git add a5; git commit -am a5
git checkout b
git rebase a # again: rebase early
echo b5>b5; git add b5; git commit -am b5
git show-branch
Note that it is in practice not hard to go back to the a branch to commit your a3/a4/a5 commits:
- in case you only realized you touched things that belong on that branch, you can just switch to the a branch with pending local changes2
- you then selectively (!!) stage the parts that needed to go onto the a branch (
git add -i
or similar in git guis or e.g.vim fugitive
) - commit on a
- switch back to the b branch (
git checkout b
) - commit the remaining bits
- rebase b onto the latest revision in a
2 as long as they don't conflict with other changes from a..b; in that case you'd git stash
first and git stash apply
when on the a branch
精彩评论