转自: https://blog.yorkxin.org/posts/2011/07/29/git-rebase/
最近刚好有个机会整理很乱的Git commit tree,终于搞懂了rebase 的用法,笔记一下。
大家都知道Git 有个特色就是branch 开很大开不用钱,但很多branches 各自开发,总要在适当时机merge 进去master 。看过很多git 操作指南都告诉我们,可以妥善利用rebase 来整理看似很乱或是中途可能不小心手滑commit 错的commits ,甚至可以让merge 产生的线看起来比较简单,不会有跨好几十个commits 的线。
首先要提一下rebase的意思,我擅自的直译是「重新(re-)定义某个branch的参考基准(base)」。把这个意思先记起来,比较容易理解rebase的运作原理。就好比移花接木那样(稼接),把某个树枝接到别的树枝。
在git 中,每一个commit 都可以长出branch ,而branch 的base 就是它生长出来的commit ,rebase 也就是把该branch 所长出来的commit 给改去另一个commit 。不过,因为rebase 会调整commit 的先后关系,弄不好的话可能会把你正在操作的branch 给搞烂,所以在做rebase 之前,最好开一个backup branch ,什么时候出差错的话,reset 回backup 就行了。
以下用实际的例子来操作比较容易解释。看log的程式是GitX (L)。
Update 2012/06/28:也可以看ihower的录影示范,实际操作会比读文字来得容易懂。
例如我要写个网页,列出课堂上的学生。我把样式的设计( style
)跟主干( master
)分开,档案有index.html
和style.css
。
到目前为止有以下的commit history:
style
完成了一小部份,而接下来要修饰的页面是master
里面有改过的,如何让style
可以继承master
呢?就是用rebase把style
branch给接到master
后面了,因为rebase是「重新定义基准点」。就像是在稼接时,把新枝的根给「接」在末梢上。
rebase的基本指令是git rebase <new base-commit>
,意思是说,把目前checkout出来的branch分支处改到新的commit。而commit可以使用branch去指(被指中的commit就是该branch的HEAD),所以现在要把style
这个branch接到master
的HEAD(dc39a81e
),就是在style
这个branch执行
git rebase master
完成之后,图变这样:
果然顺利接起来了。
而在执行的过程中会看到:
First, rewinding head to replay your work on top of it...
Applying: set body's font to helvetica
Applying: adjust page width and alignment
这是它的操作方式,照字面上的意思,就是它会尝试把当前branch的HEAD给指到你指定的commit (在这里是原本master
的HEAD,也就是dc39a81e
),然后把每个原本在style
上面的commits (d242d00c..0b373e34
)给重新commit进去style
这个branch (re-apply commits)。也由于是「重新commit」,所以rebase以后的commit ID (SHA)都不一样。
那如果过程中有conflict 呢?后文会提到。
接着再开个新的branch叫list
,专门改学生清单,同时另一个人也在改style
这个branch ,修饰网页的整体装饰。改啊改,变成这样分叉的两条线:
list
改到一个段落,没有问题了,就想merge进master
。在master
branch做
git merge list
这时git发现,刚好master
直接指到list
的HEAD commit也行,所以git直接就改了master
的commit ID ,也就是所谓的fast-forward,熟悉C语言的同学应该对这种指标移动不陌生。完成之后就是这样:
rebase --onto
:指定要从哪里开始接枝list
继续改,style
还是继续改,变这样:
现在style
要开始装饰学生清单了,而学生清单是list
这个branch在改的。于是style
应该要rebase到list
,可是这时管list
的说,我后面几个commits还没敲定,你先拿64a00b7e (add their ages)
这个commit当基准,这我改好了。所以这时候,应该要把style
这个branch接到64a00b7e
的后面。
该怎么办呢?这时就要用git rebase --onto
了。指令是
git rebase --onto <new base-commit> <current base-commit>
意思是说,把当前checkout出来的branch从<current base-commit>
移到<new base-commit>
上面,就像是在稼接时,把新枝的根给「种」在某个点上,而不是接在末梢。(这似乎也是稼接最常用的方式?有请懂园艺同学的指教一下)
再看一下commit history:
现在style
是based on dc39a81e (add some students)
,要改成based on 64a00b7e (add their ages)
,也就是
<current base-commit>
= dc39a81e
<new base-commit>
= 64a00b7e
那就来试试看
git rebase --onto 64a00b7e dc39a81e
果然达到了目的,style
现在是based on 64a00b7e
了(当然commit IDs也都不同了)。
接着改style
的人修改了学生清单的样式,可是他很机车,他要改index.html
里面的东西(实际情况是,list
里写了一个table
,但写css总要有些class
或id
的attributes才能设定)。刚好改list
的人也在他自己的branch里面改,这时候,在rebase试着re-apply commits的过程中,必定会产生conflict。
现在list
要利用到style
里面修饰好的样式,在这个情况下,就是把list
给rebase到style
上面,也就是在list
branch做 git rebase style
。不过你会看到这个:
First, rewinding head to replay your work on top of it...
Applying: add gender column
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Failed to merge in the changes.
Patch failed at 0001 add gender column
When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".
跟预期的一样出现了conflict。当然,它会先试着自动merge ,但如果改到的行有冲突,那就得要手动merge了,打开他说有冲突的档案,改成正确的内容,接着使用git add <file>
(要把该档案加进去staging area,处理rebase的程式才能commit),再git rebase --continue
。
完成以后就会像这样:
接着style
和list
又陆续改了一些东西,主要是list
里面加了表单元件,而style
则继续修饰网页整体设计。到了一个段落,该轮到style
修饰list
的表单了。目前的commit history长这样:
不过在style
要rebase到list
上面之前,管list
的人想把list
上面的一些commits给整理过,因为他发现有这些问题:
"wrap the form with div"
太后面了,想移到前面"fix typo of age field name"
跟"add student id and age..."
可以合并"add student id and age ..."
里面东西太多,该拆成两个"form to add more *studetns*"
这message有错字"studetns""add gender select box"
里面的程式码有打错字(囧上面提到了rebase 运作的方式是重新commit 过一遍,那这个「重新commit」的过程,能不能让程式设计师来干预,达到偷天换日修改commit的目的呢?当然可以,只要利用rebase的Interactive Mode。Git的灵活就在这里,连commit的内容都可以改。
如何启动interactive mode呢?只要加入-i
的参数就行了。以这个例子来说,list
branch是based on 0580eab8 (fill in gender column)
,要从这个commit后面重新apply一次commits ,也就是:
git rebase -i 0580eab8
接着会以你的预设编辑器打开一个档案叫做.git/rebase-merge/git-rebase-todo
,里面已经有一些git帮你预设好的内容了,其实就是原本commits的清单,你可以修改它,告诉git你想怎么改:
git rebase -i
pick 2c97b26 form to add more studetns
pick fd19f8e add student id and age field into the form
pick 02849bf fix typo of age field name
pick bd73d4d wrap the form with div
pick 74d8a3d add gender select box
# Rebase 0580eab..74d8a3d onto 0580eab
# ...[chunked]
第一个栏位就是操作指令,指令的解释在该档案下方有:
pick
=要这条commit ,什么都不改reword
=要这条commit ,但要改commit messageedit
=要这条commit,但要改commit的内容squash
=要这条commit,但要跟前面那条合并,并保留这条的messagesfixup
= squash +只使用前面那条commit的message ,舍弃这条messageexec
=执行一条指令(但我没用过)此外还可以调整commits 的顺序,直接剪剪贴贴,改行的顺序就行了。
首先我想要把"wrap the form with div"
移到"form to add more studetns"
后面,然后"form to add more studetns"
要改commit message (有typo),那就改成这样:
git rebase -i
reword 2c97b26 form to add more studetns
pick bd73d4d wrap the form with div
pick fd19f8e add student id and age field into the form
pick 02849bf fix typo of age field name
pick 74d8a3d add gender select box
接着储存档案后把档案关掉(如vim的:wq
),就开始执行rebase啦,遇到reword
时会再跳出编辑器,让你重新输入commit message 。这时我把studetns
改正为students
,然后就跟平常commit一样,存档并关掉档案。
git commit
form to add more students
# Please enter the commit message for your changes. Lines starting
# ...[chunked]
完成后会看到:
Successfully rebased and updated refs/heads/list.
再看commit history ,的确达到了目的,而且list
这个branch一样还是based on0580eab8
,后面那些刚刚rebase过的commits统统换了commit ID :
剩下这些要做:
"fix typo of age field name"
跟"add student id and age..."
可以合并"add student id and age ..."
里面东西太多,该拆成两个"add gender select box"
里面的程式码有打错字现在来试试看合并,一样是 git rebase -i 0580eab8
,并使用fixup
来把commit给合并到上一个(如果用squash
的话,会让你修改commit message ,修改时会把多个要连续合并的commit messages放在同一个编辑器里):
git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
pick 0d450ea add student id and age field into the form
fixup 8f5899e fix typo of age field name
pick e323dbc add gender select bo
完成后再看commit history ,的确合并了:
剩下了拆commit 和订正commit 内容。现在先来做订正commit ,这个学会了就知道怎么拆commit 了。
在这里下edit
指令来编辑commit内容:
git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
pick 53616de add student id and age field into the form
edit c5b9ad8 add gender select box
存档并关闭之后,现在的状态是停在刚commit完"add gender select box"
的时候,所以现在可以偷改你要改的东西,存档以后把改的档案用git add加进staging area ,再打
git rebase --continue
来继续,这时候因为staging area里面有东西,git会将它们与"add gender select box"
透过commit --amend
一起重新commit 。
最后是拆commit 。怎么拆呢?刚刚做了edit
,不是停在该commit之后吗?这时候就可以偷偷reset到HEAD^
(即目前HEAD的前一个),等于是退回到HEAD指到的commit的前一个,于是该commit的changes就被倒出来了,变成changed but not staged for commit,再根据你的需求,把changes给一个一个commit就行了。
实际的操作如下。首先是用edit
指令来编辑commit内容:
git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
edit 53616de add student id and age field into the form
pick 4dbcf49 add gender select box
接着使用
git reset HEAD^
来把目前的HEAD 指标给指到HEAD 的前一个,指完之后,原本HEAD commit 的内容就被倒出来,并且也不存在stage area 里面, git 会提示有哪些档案现在处于changed but not staged for commit :
Unstaged changes after reset:
M index.html
现在我可以一个一个commit了,原本是add student id and age field
,我想拆成一次加student id field ,一次加age field 。commit完成以后,再打
git rebase --continue
这次因为staging area 里面没东西,所以就继续re-apply 剩下的commits 。
现在打开log 看,拆成两个啦!
掌管list
branch的人折腾完了,便告诉管style
的说,可以rebase了,git 再度拯救了苦难程序员的一天。
更多rebase :