Git-rebase 小笔记

转自: 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 的意义:重新定义参考基准

首先要提一下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.htmlstyle.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接到masterHEADdc39a81e),就是在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 (在这里是原本masterHEAD,也就是dc39a81e),然后把每个原本在style上面的commits (d242d00c..0b373e34)给重新commit进去style这个branch (re-apply commits)。也由于是「重新commit」,所以rebase以后的commit ID (SHA)都不一样。

那如果过程中有conflict 呢?后文会提到。

fast-forwarding: 可以的话,直接改指标,不重新commit

接着再开个新的branch叫list,专门改学生清单,同时另一个人也在改style这个branch ,修饰网页的整体装饰。改啊改,变成这样分叉的两条线:

image

list改到一个段落,没有问题了,就想merge进master。在master branch做

git merge list

这时git发现,刚好master直接指到listHEAD commit也行,所以git直接就改了master的commit ID ,也就是所谓的fast-forward,熟悉C语言的同学应该对这种指标移动不陌生。完成之后就是这样:

image

rebase --onto :指定要从哪里开始接枝

list继续改,style还是继续改,变这样:

image

现在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:

image

现在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

image

果然达到了目的,style现在是based on 64a00b7e了(当然commit IDs也都不同了)。

conflict 的处理

接着改style的人修改了学生清单的样式,可是他很机车,他要改index.html里面的东西(实际情况是,list里写了一个table,但写css总要有些classid的attributes才能设定)。刚好改list的人也在他自己的branch里面改,这时候,在rebase试着re-apply commits的过程中,必定会产生conflict。

image

现在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

完成以后就会像这样:

image

Interactive Mode: 偷天换日,自定重新commit 的详细步骤

接着stylelist又陆续改了一些东西,主要是list里面加了表单元件,而style则继续修饰网页整体设计。到了一个段落,该轮到style修饰list的表单了。目前的commit history长这样:

image

不过在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 message
  • edit =要这条commit,但要改commit的内容
  • squash =要这条commit,但要跟前面那条合并,并保留这条的messages
  • fixup = squash +只使用前面那条commit的message ,舍弃这条message
  • exec =执行一条指令(但我没用过)

此外还可以调整commits 的顺序,直接剪剪贴贴,改行的顺序就行了。

调整commit 顺序、修改commit message

首先我想要把"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 :

image

合并commits

剩下这些要做:

  • "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 ,的确合并了:

image

修改、拆散commit 内

剩下了拆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 看,拆成两个啦!

image

掌管list branch的人折腾完了,便告诉管style的说,可以rebase了,git 再度拯救了苦难程序员的一天

image


更多rebase :

posted on 2016-03-04 11:41  醉清风JM  阅读(5105)  评论(1编辑  收藏  举报

导航