整理下 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 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 rebasegit rebase 还有很多用法, 这里不详细说明, 要用时再了解就好了
git checkout & git reset & git revert
本节插图来自 Resetting, Checking Out & Reverting
这三个指令都可以做到撤销改动的作用, 但是背后的行为是不一样的, 结果也有所不同, 其中 git checkout 肯定是大家最熟悉的这里分别按顺序介绍下三个指令的作用以及背后做了什么
git 的本地的三个区域
分别称为:
工作区
暂存区
版本区
这三个区分别管理着 git 项目中的改动的不同阶段的状态
git checkout
git checkout 是工作中常见的一个操作
commit 操作
在对 commit 操作的时候, 简单来说就是将 HEAD(HEAD 我的理解是一个指向当前活跃状态或者说当前活跃 commit 的一个指针, 表示的是现在做出的版本状态)移动到对应的 commit, 当你 checkout 一个分支的时候, 则是将 HEAD 指针移至这个分支的最新的一个 commitgit checkout 并不会影响分支上的 commit, 而只是切到对应的 commit 的版本状态, 在切换之前, 需要保存当前的改动并且 commit, 因为你一旦切走了, 虽然没有改动 commit 的历史, 之后也可以切回来, 但是 checkout 走了之后, 你的 HEAD 就不指向当前分支最新的状态了, 这时候需要将改动保存, 之后最为一个 commit 版本来进行管理用图来表示如下:
只是改变了 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 reset
git reset 这个操作可以对文件也可以对 commit 进行操作, git reset 需要慎用, 因为 git reset 是会改动到 commit 的历史的
文件操作
git reset 指定文件的时候会将缓存区同步到你指定的那个提交 git reset 默认 reset 到 HEAD, 所以可以用来移除暂存区的指定文件 --soft--mixed 和 --hard 对文件层面的 git reset 的作用没有区别, 缓存区中的文件会变化, 而工作目录中的文件不变
commit 操作
git reset 的撤销操作是真 . 撤销操作, git reset 将一个分支的末端指向另一个提交这可以用来移除当前分支的一些提交被移除的 commit 在下次 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
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, 处理起来比较麻烦
用图来解释如下:
参考资料:
- Merging vs. Rebasing
- Resetting, Checking Out & Reverting
git reset soft,hard,mixed 之区别深解
Git fast forward VS no fast forward merge
来源: https://juejin.im/post/5abb3ece6fb9a028e0148557