Git 中那些容易混淆和忽略的指令都做了些神马

1,906 阅读9分钟

整理下git中不是很清晰的一些指令的背后做了什么,有说的不对的地方,欢迎指正。

git merge & git rebase

这节缘起于最近给公司的优秀的开源组件库提pr,在开发说明文档中看到,推荐在提交pr前建议用git rebase 清理下commit再提pr~。恩,平时合并改动的时候git merge是老朋友,常用到,介个git rebase有听说,但是用的不多。特此了解下两者的区别,简单记录下吧。

首先git merge 和 git rebase 都可以合并两个分支的commit。

git merge

举个栗子:

git checkout feature
git merge master

feature上除了合并了master分支上改动的commit外会多一个merge的commit说明这次merge,此外不会生成新的commit

git rebase

举个栗子:

git checkout feature
git rebase master

feature上的新的commit会全部重新以master最新的commit为base,效果就是feature上的新的commit就像是在最新的master上checkout出来的分支上后续添加的commit,这些commit是新生成的并且不会有那个专门说明合并的commit。【有些人会觉得这种commit污染了commit的记录~,但是也有人觉得这个merge commit可以帮忙理解到每次合并操作的发生的时间和合并的分支,这些信息是有用的。恩,我都好~

引用很漂亮的图来解释如下【图来自《Merging vs. Rebasing》】:

git

git

git

划重点,其实需要注意的点在于:

  • git merge 除了merge commit外,合并到feature上的commit不是生成的新的commit。
  • git rebase 则是线性的将feature的commit接到master的最新的commit后面,以新生成的commit的形式。

这有个问题,举个栗子:

就是git rebase的feature分支如果是一个多人开发的分支,那么rebase的是你本地的分支,合作的小伙伴的分支和你的这个分支进行同步的话,会有一些内容重复的但是commit的hash值不一样的多余的commit,整个commit就冗余不干净了~

因此比较好的使用git rebase的场景是单人开发的分支,或者使用一个临时的分支进行git rebase 然后使用git merge到master分支上,merge master的时候是fast forward的,并且不会有master到feature的merge commit。

此外

  • git rebase 的交互模式,即:git rebase -i 可以进行更细粒度的rebase过程中的commit的挑选和合并,也很方便。
  • 你也可以rebase分支本身,比如3个commit之前为base进行rebase,这样相当于整理最近的3个commit的效果。

在知道了这些重要的点后,就可以自行判断何时应该使用git merge 还是 git rebase。git rebase还有很多用法,这里不详细说明,要用时再了解就好了。


git checkout & git reset & git revert

【本节插图来自Resetting, Checking Out & Reverting

这三个指令都可以做到撤销改动的作用,但是背后的行为是不一样的,结果也有所不同,其中git checkout肯定是大家最熟悉的。这里分别按顺序介绍下三个指令的作用以及背后做了什么。

git的本地的三个区域

git

分别称为:

  • 工作区
  • 暂存区
  • 版本区

这三个区分别管理着git项目中的改动的不同阶段的状态。

git checkout

git checkout是工作中常见的一个操作。

commit操作

在对commit操作的时候,简单来说就是将HEAD(HEAD我的理解是一个指向当前活跃状态或者说当前活跃commit的一个指针,表示的是现在做出的版本状态)移动到对应的commit,当你checkout一个分支的时候,则是将HEAD指针移至这个分支的最新的一个commit。git checkout并不会影响分支上的commit,而只是切到对应的commit的版本状态,在切换之前,需要保存当前的改动并且commit,因为你一旦切走了,虽然没有改动commit的历史,之后也可以切回来,但是checkout走了之后,你的HEAD就不指向当前分支最新的状态了,这时候需要将改动保存,之后最为一个commit版本来进行管理。用图来表示如下:

git

只是改变了HEAD的指向,并没有改动到commit的历史,这个时候工作区和暂存区的状态保持一致为checkout到的这个版本的状态。

也就是说,git checkout对commit操作的时候的作用为查看历史版本,当然你也可以在这时候进行改动并且commit,这样操作的结果就是,在checkout到的commit的基础上多了一个没有归属任何分支的commit。当你这时checkout回其他分支的时候,git会提醒你给这个刚才在'detached HEAD' state时新增的commit的那个改动的分叉创建一个分支,这样方便之后切到这个状态,而不是一个没有branch归属的commit改动。

文件操作

git checkout对文件进行操作的时候,则是将工作区的指定的文件或者目录的状态切换成指定的版本的状态,对暂存区和版本区没有影响。如果你git checkout一个文件的时候默认是HEAD,产生的效果就是放弃工作区当前的改动,这个也是小伙伴们使用git checkout比较多的操作之一。

git revert

git revert只能操作commit,不能对文件进行操作,git revert的撤销背后的原理,就是用一次新的反向的commit将工作区、暂存区、版本区的状态全部回退到指定的版本。也就是你的commit历史会多一个commit,这个commit的操作就是指定的那些撤销改动。git revert不会对历史的commit进行改动,因此常用与公共分支的回滚。保留了commit历史,同时也完成了版本回滚,并且其他小伙伴在同步这次的回滚的时候只是相当于fast forward了一个commit版本。用图来表示如下:

git

git reset

git reset这个操作可以对文件也可以对commit进行操作,git reset需要慎用,因为git reset是会改动到commit的历史的。

文件操作

git reset指定文件的时候会将缓存区同步到你指定的那个提交。git reset默认reset到HEAD,所以可以用来移除暂存区的指定文件。文件层面只支持--mixed参数,作用就是unstaged对应的暂存区中的文件。

commit操作

git reset的撤销操作是“真 · 撤销”操作,git reset 将一个分支的末端指向另一个提交。这可以用来移除当前分支的一些提交。被移除的commit在下次 git 执行垃圾回收的时候会被删除。换句话说,如果你想彻底的扔掉提交,你可以这么做。用图来表示如下:

git

git reset会改写当前分支的commit的历史,所以最好不要在公共分支上进行这个操作。git reset 操作有三个选项来指定这个操作的影响范围或者说作用域:

  • --soft – 缓存区和工作目录都不会被改变
  • --mixed – 默认选项。缓存区和你指定的提交同步,但工作目录不受影响
  • --hard – 缓存区和工作目录都同步到你指定的提交

《git reset soft,hard,mixed之区别深解》中的解释,我觉得挺清晰的,参考总结如下:

  • --soft参数告诉Git重置HEAD到另外一个commit,但也到此为止。所有的在original HEAD和你重置到的那个commit之间的所有变更集都放在暂存区中。

  • --hard参数将会blow out everything.它将重置HEAD返回到另外一个commit,重置暂存区以便反映版本区的变化,并且重置工作区也使得其完全匹配起来。这是一个比较危险的动作,具有破坏性,数据因此可能会丢失(makes everything matching the commit you have reset to.)。如果真是发生了数据丢失又希望找回来,那么只有使用:git reflog命令了。

  • --mixed是reset的默认参数,也就是当你不指定任何参数时的参数。它将重置HEAD到另外一个commit,并且重置暂存区以便和版本区相匹配,但是也到此为止。工作区不会被更改,所有该branch上从original HEAD(commit)到你重置到的那个commit之间的所有变更将作为local modifications保存在工作区中,(被标示为local modification or untracked via git status),但是并未staged的状态,你可以重新检视然后再做修改和commit。

总结表格

【图表来自参考资料文章】

git


fast forward & non fast forward

在合并分支的时候git merge是老朋友,一般的小伙伴都是直接git merge 巴拉巴拉吧就解决了,这种情况下默认使用的是fast forward模式进行分支的合并。另外我们可以在git merge的时候加上--no-ff参数来切换成non fast forward模式进行分支的合并。这两种合并方式的结果不同,适用于不同的场景。

fast forward

fast forward简单来说,就是将分支改动的commit按照时间顺序依次并入到(举个栗子)master分支上,合并的分支和master分支是一个扁平的关系。这里有个问题就是,commit可能和master上新增的commit穿插到一起,并且,在branch tree上,没办法直观的看到一个分支的所有改动都有哪一些,因为这些改动被插入到master的commit历史中了。

non fast forward

non fast forward简单来说,合并分支的时候一定会多产生一个merge commit来说明这次合并的操作。并且在branch tree上被合并的分支和master分支的关系不是扁平的,是可以清晰的看到这个合并的操作合并了哪些commit。此外,non fast forward的合并的回滚也比较方便,只要revert到对应的这个merge commit就好了。不会有fast forwar的模式下回滚的时候影响的commit穿插在其他正常改动的commit中间,这时候,回滚就很难办,可能会回滚掉正常的commit,在公共开发的分支上进行rebase整理commit历史也不好(原因在git rebase中已经说明了),整个处理起来就比较麻烦。

用图来解释如下:

git


参考资料: