Git应用详解第九讲:Git cherry-pick与Git rebase
source link: http://www.cnblogs.com/AhuntSun-blog/p/12732946.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
前言
前情提要: Git应用详解第八讲:Git标签、别名与Git gc
这一节主要介绍 git cherry-pick
与 git rebase
的原理及使用。
一、 Git cherry-pick
Git cherry-pick
的作用为移植提交。比如在 dev
分支错误地进行了两次提交 2nd
和 3rd
,如果想要将这两次提交移植到 master
分支上。采用先删除再添加的方法将会很繁琐,而使用 cherry-pick
就能轻松实现这一需求。
首先在版本库中创建了两个分支 master
和 dev
,并模拟上述场景:
可以看到,在 dev
分支上进行了两次提交,在 master
分支上只进行了一次提交。现在想要将这两次提交 移植 到 master
分支上。整体分为两步:
- 第一步: 将
dev
分支上多余的两次提交移植到master
分支上; - 第二步: 删除
dev
分支上多余的两次提交;
1.第一步
git cherry-pick commit_id
首先切换到 master
分支,然后使用如下命令将 dev
分支上的两次提交移植到 master
分支上:
//移植2nd提交 git cherry-pick 009dd //移植3rd提交 git cherry-pick aec8c
009dd
和 aec8c
分别表示需要移植的提交 2nd
和 3rd
的 SHA1
值:
移植过程为:
-
如上图所示,执行了两次
cherry-pick
指令,创建了两个内容与2nd、3rd
一致的提交对象50477
和f05a0
。所以,cherry-pick
指令移植提交的实质是:先将需要移植的提交复制一份,再拼接到master
分支上,简称 先复制,再拼接 ; -
上面按照顺序先移植了提交
2nd
再移植提交3rd
,不会发生冲突; -
不按顺序移植,如先移植提交
3rd
会发生 合并冲突 ,需要手动解决:
通过 vi test.txt
查看发生合并冲突的 test.txt
文件:
可以发现 master
分支上 initial commit
提交中的文件 test.txt
直观上 并不与提交 3rd
中的 test.txt
冲突,如下图所示:
但是为什么会发生合并冲突呢?原因在于 三方合并原则 :
如上图所示,当想要将 dev
中的提交 E
与 master
分支的提交 B
合并时,首先要找到 B
和 E
的公共父节点 A
,在 A
的基础上根据 B
和 E
进行三方合并;
了解了三方合并原则后就能解释上面发生合并冲突的原因了:
-
由于提交
3rd
是基于提交2nd
创建的,因此3rd
中保留了2rd
中对文件的操作记录; -
如果直接将
3rd
拼接到initial commit
后面,就会失去提交2nd
的记录; -
由此提交
3rd
就不能通过提交2nd
找到公共提交节点init
,这就会导致合并失败;
所以,无论内容是否冲突,合并过程都会出现冲突:
解决方法:手动合并三步曲:
- 首先,选择要保留的内容,解决冲突:
- 然后,通过
git add
将修改信息纳入暂存区:
- 最后,通过
git commit
提交修改信息:
完成后查看 master
分支的提交历史:
可以看到解决冲突,手动合并后,成功完成了整个 cherry-pick
过程。并且新增的提交是手动合并时进行的提交,而不是直接复制的提交 3rd
:
2.第二步
此时两分支的状态为:
接下来就要删除 dev
分支上错误的两次提交 2nd
和 3rd
,相当于版本回退;可以使用三种方法: revert
、 reset
和 checkout
,这里演示 checkout
和 reset
两种方法。
使用 checkout
首先切换到 dev
分支,然后通过以下指令切换到提交 initial commit
:
//dd703是提交initial_commit的SHA1值 git checkout dd703
此时该节点处于游离状态:
然后再删除 dev
分支:
由于之前修改的 dev
分支没有与 master
进行合并,所以删除时需要使用参数 -D
强制删除。
删除后,剩下 master
分支与游离提交。此时再通过以下指令将游离的节点设置为 dev
分支即可:
git checkout -b dev
由此通过" 偷天换日 "的方式使 dev
分支回到了错误提交前的状态;
使用 reset
由于使用 checkout
只是移动了 HEAD
指针,没移动 dev
分支指针,所以会出现游离提交节点;而 reset
会同步移动 HEAD
和 dev
分支指针,不会造成这样的问题。所以这里使用 reset
进行版本回退会简单很多:
git reset --hard dd703
二、 git rebase
简介
首先, rebase
有两个意思: 变基 、 衍合 ,即变换分支的参考基点。默认情况下,分支会以分支上的第一次提交作为基点,如下图所示 master
分支默认以提交 1st
作为基点:
如果以提交 4th
作为 master
分支的基点, master
分支就会变为:
这个变化基点的过程就称之为变基( rebase
);
rebase
与 merge
十分相似,不过二者的工作方式有着显著的差异。比如:将 A
和 B
两分支进行合并:
- 在
A
分支上执行git merge B
,表示的是将B
分支 合并到A
分支上; - 而在
A
分支上执行git rebase B
,则表示将A
分支通过变基 合并到B
分支上;
三、 merge
与 rebase
1.采用 merge
合并分支
现在有两个分支 origin
和 mywork
,如果想要将 origin
分支 合并到 mywork
分支上。根据三方合并原则,需要在 c4
、 c6
和它们的公共父提交节点 c2
的基础上进行合并:
合并后产生一次新的提交 c7
,该提交有两个父节点 c4
和 c6
。具体的合并方式为:如果没有冲突 git
就会自动采用 Fast-forward
方式进行合并,有冲突就解决冲突再进行手动合并。
2.采用 rebase
合并分支
由于是 mywork
分支需要变基合并到 origin
分支上,所以首先切换到 mywork
分支(注意这里与采用 merge
方法时所在的分支相反):
git checkout mywork
再进行合并:
git rebase origin
合并后的结果为:
注意:被合并的分支 origin
保持不动 ,而合并它的分支 mywork
将自己的提交作为补丁( patch
)一个个应用( applying
)到分支 origin
指向的提交后面;
在这个过程中 git
会自动创建 c5'
和 c6'
。原来的 c5
和 c6
就没用了,会被 git gc
回收。合并后分支 mywork
的提交记录变成了一条直线:
也就是说: rebase
会将被合并分支( mywork
)上的提交应用到合并分支( origin
)上,并且修改被合并分支( mywork
)的提交记录。
四、 rebase
原理分析
如图所示, master
和 dev
分支都以提交节点 A
为基准点:
如果 dev
分支想要变换 A
这个基准点,那么:
第一步:切换到 dev
分支上;
第二步:执行 git rebase master
,过程如下;
上述命令中 rebase
参数后面指定的就是变更后的基准点:
- 如果是分支,如
master
,基准点为该分支的最新提交节点,也就是C
; - 如果是一个
commit_id
,基准点为该commit_id
对应的提交节点;
1.基准点为分支
沿用以上模型:
- 首先,将
dev
分支上除了基准点A
外的所有节点复制一份,即D'
和E'
,作为补丁备用,并将分支dev
指向新基准点C
:
- 然后,按原来
dev
上的节点顺序(D->E
)将补丁应用(Patch Applying
)到新基准点C
后面,并同时改变分支dev
指向:
追加补丁 D'
:
每次向新基准点应用补丁时,都会出现 三个选项 :
git rebase --continue
该选项表示:解决了合并冲突后,继续应用剩余补丁 E'
:
git rebase --skip
该选项表示:跳过当前补丁,继续应用下一个补丁:
如果一直执行该选项,直到应用完分支 dev
上的补丁,结束 rebase
后,两分支的状态为:
git rebase --abort
该选项表示:终止 rebase
操作,回到执行 rebase
指令前的状态:
2.基准点为提交
过程详解
如图所示,若将提交节点 B
作为基准点,在当前 test
分支上执行:
git rebase 3ccc8
会直接将原来的节点 C
和 D
应用到新基准点 B
后,相当于没有发生变化,这个变基的过程为:
- 首先,将基准点和
test
分支指向改变为节点B
,并将test
分支上基准点往后的提交节点作为补丁:
- 然后,按顺序将补丁
C
和D
应用到新基准点B
后面:
- 最后,
test
分支的状态为:
所以,直接执行 git rebase 678e0
不会有任何变化:
但是,我们可以通过在 rebase
中添加参数 -i
,进入 rebase
交互模式,这样就能在 rebase
操作过程中对特定的补丁进行一系列操作;
实战演示
首先在 test
分支上进行了四次提交:
执行以下指令将 test
分支的基准点变为提交节点 B
( 678e0
),并进行变基:
git rebase -i 678e0
执行该指令后,会进入 vim
编辑器:
可以根据需要将 pick
参数,改变为下面代表不同作用的参数;这样就可以对节点 C
和 D
进行不同的操作了。比如:
-
pick
:默认参数,表示不对提交节点进行任何操作,直接应用原提交节点。不创建新提交; -
reword
:应用复制过后的原提交节点,但是可以编辑该节点的提交信息。通过这个参数,可以修改特定提交的提交信息。会创建新的提交; -
edit
:应用复制过后的原提交节点,会在设置了该参数的补丁上停止rebase
操作。待修改完该补丁后,调用git rebase --continue
继续进行rebase
。会创建新的提交; -
squash
:将新基点后面的全部提交节点进行合并,也就是将这里的C
和D
两个节点进行合并。会创建新的提交; - 还有其他参数这里就不一一介绍了。
这次直接使用默认的 pick
参数,通过 :wq
保存并退出 vim
编辑器,完成 rebase
操作:
执行 rebase
操作前:
可以看到当新基准点为特定提交时:
- 在
rebase
的过程中使用默认参数pick
,并不会像当新基准点为分支时那样创建新的提交; - 而一旦使用其他参数(如
reword
)对补丁进行了修改,就会创建新的提交;
五、 rebase
注意事项
-
不要对
master
分支执行rebase
,否则会引起很多的问题(master
一定是远程共享的分支); -
一般来说,执行
rebase
的分支都是自己的本地分支,千万不要在与其他人共享的远程分支上使用rebase
;这不难理解,远程分支上的代码可能已经被其他人克隆到本地了,如果通过
rebase
修改了远程分支的提交历史,这样其他人每次拉取代码到本地时,就都需要进行复杂的合并。 -
所以,本地的非
master
分支合并时推荐使用git rebase
,其他分支的合并推荐使用git merge
;
注意: git merge
和 git rebase
的显著区别是,前者不会修改 git
的提交记录,而后者会!
六、 rebase
应用场合
1.合并分支
由于 git merge
采用的是 三方合并 的原则,没有公共提交节点就无法进行合并,此时可以采用 rebase
进行合并。如下图所示:
本地 master
与远程 master
分支没有公共提交节点,无法采用 git merge
合并。可采用 rebase
进行合并:
//origin/master代表着远程master分支 git rebase origin/master
合并后本地 master
分支的状态为:
2.修改特定提交
以下情况就适合使用 rebase
来解决,当回退版本并进行修改时:
比如在 master
分支上进行了 3
次提交:
回退到第二次提交 2nd
,并对提交信息进行修改:
当我们回到原来的第三次提交 3rd
时,会发现之前的修改并没有被保存:
此时可以使用 rebase
,将提交 1st
作为新的提交节点(正如第四大点讲解的)。首先执行:
git rebase -i 5ab3f
通过添加参数 -i
进入交互模式,将提交 2nd
默认的 pick
参数修改为 reword
参数:
保存并退出后,进入修改提交信息界面:
保存并退出,由此完成修改:
七、 rebase
实战
为了演示,额外创建两个分支 dev
和 test
,分别在两个分支上进行两次提交:
它们有一个共同的父节点提交节点 init
,此时本地仓库的状态如下:
-
由于要对
test
分支进行变基,从而 合并到dev
分支上,所以需要先切换到test
分支上,这与merge
操作是相反的; -
随后在
test
分支上执行如下命令对该分支进行变基:
git rebase dev
该指令翻译过来就是:我 test
分支,现在要重新定义我的基准点,即使用 dev
分支指向的提交作为我新的基准点。过程如下:
-
首先,将
test
分支上的提交(补丁)tes1
应用到 新基准点dev2
尾部,出现了合并冲突:查看状态,发现
test
分支变基过程中的新基准点正是dev
分支指向的提交361be
,即提交节点dev2
:
如图所示,此时有三个选项:
-
选项一:
git rebase --abort
:表示终止rebase
操作,恢复到操作前; -
选项二:
git rebase --skip
:表示丢弃当前test
分支的补丁,如果一直执行该选项,变基完成后,两分支的状态如下所示:即此时
test
分支与dev
分支上具有相同的文件:并且
test
分支上的提交记录被改变为了dev
分支上的提交记录:这就是一直执行选项
git rebase --skip
,丢弃全部test
分支补丁的结果: -
选项三:
git rebase --continue
:解决冲突,手动合并后,继续变基;在
dev
分支上新增两次提交dev3
和dev4
:切换回
test
分支同样新增两次提交tes3
和tes4
:此时两分支的状态为:
随后在
test
分支上执行git rebase dev
,在处理test
分支上的第一个补丁tes3
时出现冲突:打开冲突文件
test.txt
,手动解决冲突:删除
4、7、9
行:解决冲突后,执行
git add
将对文件``test.txt`的修改操作纳入暂存区,标识已解决冲突:注意:这里并不需要进行一次提交,继续执行
rebase
操作即可;随后再执行
git rebase --continue
,继续处理test
分支的下一个补丁(变基):rebase
结束后,查看test
分支的提交记录:可以发现修改了
test
分支的提交历史,达到了预期的合并效果。并且,此时
test
分支上的tes3
与tes4
两次提交的SHA1
值与执行rebase
前这两次提交的SHA1
值是不一样的:这也就验证了,
git
在rebase
过程中会自动创建提交节点的结论。此时dev
分支与test
分支的状态如下所示:如果在
dev
分支上执行git merge test
,采用的应当是Fast-forward
方式:使用
gitk
可以更加直观地表示这一状态:
细心的你可能已经发现了, rebase
与 cherry-pick
十分类似。只不过 cherry-pick
不会修改分支提交记录,而 rebase
会。
八、 merge
与 rebase
的选择
使用 rebase
时要遵循 rebase
的黄金法则:永远不要在公共分支上使用 rebase
。公共分支可以理解为 master
分支。由于 rebase
会重写分支提交记录,因此会给项目的回溯带来危险。以下为它与 merge
的区别:
-
merge
是一个合并操作,使用git merge
提交历史会出现分叉,显得不是那么简洁。但是,它的好处在于不会修改任何一次提交,会完整地将所有的提交都保存下来,方便回溯。 并且只能合并有公共提交节点的分支; -
rebase
是没有合并操作的,它只是将当前分支所做的修改复制到了目标分支的最后一次提交上。所以可以不受 三方合并原则 约束,合并没有公共提交节点的分支;使用
rebase
会修改提交历史,得到的分支提交历史更加整洁。就好像写书,只会出版最终版本,之前的书稿并不会出版。但是,一定要注意 不能在共享的分支上使用rebase
。
二者都是很强大的分支整合命令,使用哪个由具体情境决定。
九、 rebase
、 reset
、 revert
这三个指令的名字很像,容易混淆,下表对比了它们的用途以及区别:
指令 改变提 交历史 用途 Reset 是 把目前分支的状态设定成某个指定的Commit
状态,通常适用于尚未推送的 Commit
Rebase
是
不管是新增、修改、删除 Commit
都相当方便。可用来整理、编辑还未推送的 Commit
,通常也只适用于尚未推送的 Commit
Revert
否
新增一个 Commit
来反转(取消)另一个 Commit
内容,原本的 Commit
依旧会保留在提交历史中。虽然会因此而增加 Commit
数,但通常比较适用于已经推送的 Commit
,或者不允许使用 Reset
或 Rebase
指令修改提交历史的场合
十、 git
最佳实践
学到这里就可以完全理解使用 git
将本地仓库文件推送到远程仓库的一般步骤了:
-
第一步:创建本地仓库:
git init
-
第二步:添加用户信息:
git config --global user.name '张三' git config --global user.email '[email protected]'
-
第三步:添加远程仓库地址:
git remote add origin https://www.github.com/example
-
第四步:修改文件;
-
第五步:将工作区中的文件纳入暂存区:
git add .
-
第六步:将暂存区中的文件提交到版本库:
git commit -m '注释'
-
第七步:与远程仓库进行同步:
git pull --rebase origin master
-
第八步:建立本地分支与远程分支的联系,并进行推送:
git push -u origin master
通过这一节的学习,相信你已经熟练掌握了 cherry-pick
和 rebase
的原理及使用方法了。下一节将会介绍 Git
子库: submodule
与 subtree
。期待与你再次相见!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK