🌳🚀 CS 可视化:有用的 Git 命令
虽然 Git 是一款非常强大的工具,但我想大多数人都会同意我的这个说法:它有时也可能是...一场噩梦😐 我一直觉得,使用 Git 时,在脑海中勾勒一下正在发生的事情是非常有用的:当我执行某个命令的时候,各个分支是如何交互的?当我在 master
分支上执行硬重置(hard reset)、强制推送(force push)到 origin 并完全删除 .git
文件夹时,为什么我的同事会哭呢 😭?
我认为为常用命令创建一些可视化示例非常契合上面描述的场景!🥳 需要说明的是,我即将介绍的许多命令都有可选的参数,你可以使用这些参数来改变它们的行为。在我的示例中,我只会演示命令的默认行为,而不会色涉及(太多的)配置选项!😄
合并
创建多个分支对于隔离不同的变更,确保你不会意外地把未经批准或已损坏的变更推送到生产环境是极其方便的。
将变更从一个分支转移到另一个分支的一种方式是执行 git merge
!Git 支持两种类型的合并:⏩ 快进式合并(fast-forward) 与 🐢 非快进式合并(no-fast-forward )。
现在你可能还不太明白,下面就让我们来看下它们的区别!
快进式合并(--ff
)
相比于要合并的分支,如果当前分支没有额外的提交记录,那么此时的合并类型就是快进式合并。其实,Git 是...一个懒惰的家伙,合并时它的首选项就是快进式提交。因为这种类型的合并不会创建新的提交记录,只是把要合并分支上的提交平移到当前分支 🥳
完美!现在我们在 master
分支上也拥有了 dev
分支上的所有提交记录。那么,非快进式合并 又是怎么回事呢?
非快进式合并(--no-ff
)
如果你的当前分支和要合并的分支对比,没有任何额外的提交,这是最好不过的了,但是这种情况很少见。如果我们在当前分支提交了一些变更,而在要合并的分支上没有这些提交,那么 Git 就会执行 非快进式合并。
伴随着一个非快进式合并,Git 会在当前分支上新建一个 合并提交 记录,这个提交的父提交既指向当前分支,又指向我们想合并的分支。
这也没什么大不了的,完美合并!🎉 现在 master
分支上包含了我们在 dev
分支上所做的全部变更。
合并冲突
尽管 Git 很擅长决定如何合并分支以及向文件添加更改,但是它并不总能够自行做出此决定🙂 当我们尝试合并的两个分支在同一文件的同一行上进行了修改时,或者一个分支删除了另一个分支修改的文件等,就会发生这种情况。
这个时候,Git 会请求你来帮助决定要做出什么样的选择。假设在某两个分支上,我们都编辑了 README.md
中的第一行。
如果我们将 dev
合并到 master
中,这最终会导致合并冲突:你想要的标题是 Hello!
还 Hey!
呢?
当你尝试合并这两个分支时,Git 会显示冲突发生的位置。我们可以手动删除不想保留的更改,保存、再次添加更改的文件到暂存区,然后提交更改🥳
耶!虽然合并冲突常常很烦人,但这是完全合理的:Git 不应该假设我们想保留哪条改动。
变基
我们刚刚看到了如何通过执行 git merge
将变更从一个分支应用到另一个分支,这里还有另一种方式,那就是执行 git rebase
,即变基操作(译者注)。
git rebase
复制 当前分支的提交(译者注:重新生成的提交),然后把这些复制的提交放到指定分支的顶部。
太好了,现在 master
分支上的所有变更在 dev
分支上都可见了!🎊
变基与合并最大的不同就是,Git 不会尝试找出哪些文件需要保留,哪些不需要保留,我们正在变基的分支总是拥有我们想要保留的最新变更。这种方式不会碰到任何的合并冲突,并能保持一个良好的线性 Git 历史。
译者注
上段内容完全是按照原文翻译的,但这里的描述是错误的,变基也有可能遭遇冲突,详情可参阅 GitHub 文档 解决 Git 变基后的合并冲突。
上例展示了变基到 master
分支的场景,然而在较大的项目中,你通常不会那样做。由于复制的提交的 SHA-1 哈希是新建的,所以 git rebase
会改变项目的提交历史。
当你正工作在某个功能(feature)分支上,期间主分支更新了,这时变基操作就很有用了。你可以在你的分支上获取到所有的更新,这可以避免将来的合并冲突!😄
交互式变基
通过 交互式变基,我们可以在将当前分支的提交变基之前修改它们!😃 另外,交互式变基对于你想要修改当前工作分支的某些提交也很有用。
我们可以对要变基的提交执行以下 6 种动作:
reword
- 改变提交信息。edit
- 修订提交。squash
- 将某个提交并入它的前一个提交。fixup
- 行为类似squash
,但是合并后的提交不保留提交信息。exec
- 在我们想要变基的每个提交上运行命令。drop
- 删除提交。
太棒了!这样,我们就能完全控制我们的提交。如果我们想删除某个提交,只需要像下面那样使用 drop
。
抑或,如果我们想把多个提交合并在一起,以获得更清晰的历史记录,也没问题!
交互式变基让你对要变基的提交有很大的控制权,甚至可以控制当前活动分支上的提交!
重置
有时我们可能会提交一些后来我们不想要的变更,它可能是一个 WIP
(Work In Process) 提交,也可能是引入了 bug 的提交!🐛 在这种情况下,我们可以执行一个 git reset
命令。
译者注
git reset
本质上是更改 HEAD 指向。
git reset
可以去除当前所有已暂存的文件,以及让我们控制 HEAD
的指向。
软重置(Soft reset)
软重置 是将 HEAD
指向某个特定提交(或者是相对于 HEAD
的提交的索引),而保留暂存区和工作区的内容。
假设我们既不想保留添加了 style.css
文件的提交 9e78i
,也不想保留添加了 index.js
文件的提交 035cc
。但是,我们却想保留新添加的文件 style.css
和 index.js
。这个场景就是使用软重置的典型用例。
当此时执行 git status
时,你会看到我们仍然可以访问之前提交所做的所有更改。这真是太棒了,因为这样一来我们就可以修复这些文件的内容,稍后再次提交它们!
硬重置(Hard reset)
有时,我们不想保留某些提交引入的变更。与软重置的行为结果不同,我们不再需要访问这些变更。Git 应该能够轻易地将仓库重置回某个提交所处的状态:这甚至包含工作目录和暂存区的更改!💣
看吧,Git 已经丢弃了 9e78i
和 035cc
这两个提交引入的变更,并将其重置到了 ec5be
所处的状态。
回退
撤销变更的另一种方式是执行 git revert
。回退到某个提交会 新建一个提交,该提交包含了回退需要做的变更!
假设 ec5be
添加了 index.js
文件。后来,我们意识到实际上不再需要它!那就来回退 ec5be
这个提交吧。
不错!提交 9e78i
恢复了由提交 ec5be
引入的变更。为了撤销某个提交而不修改分支的历史,git revert
再合适不过了。
拣选
当某个分支的某个提交包含了我们在当前活跃分支上需要的变更时,我们可以使用 cherry-pick
命令。cherry-pick
时会在当前分支上创建一个新的提交,该提交包含了我们拣选的那个提交所做的变更。
假设 dev
分支上的 76d12
提交为 index.js
文件添加了一项变更,而我们希望将此变更应用到 master
分支上,并且还不想要 dev
分支上的所有变更,我们只关心这一个提交!
泰库辣!现在主分支包含了 76d12
引入的变更。
远程获取
如果我们有一个远程 Git 分支,比如 GitHub 分支,它可能包含了当前本地分支没有的提交!也许是因为另一个分支被合并了,也许你的同事推送了一个 quick fix,种种原因吧。
通过在远程分支上执行 git fetch
命令,我们可以在本地获取这些变更!这不会以任何方式影响到你的本地分支:fetch
只是简单地下载新数据。
现在,我们可以看到远程分支中自上次推送以来所做的所有更改了!接下来,我们可以决定如何处理本地的新数据了。
拉取
虽然 git fetch
对于获取某个分支的远程信息非常有用,但是我们也可以执行 git pull
。实际上,git pull
包含了两条命令:一个是 git fetch
,一个是 git merge
。当我们从源端拉取变更时,首先做的是像使用 git fetch
那样获取数据,然后自动将最新修改合并到本地分支上。
太棒了,我们现在与远程分支完美同步,拥有了所有的最新更改!🤩
参考日志
人非圣贤,孰能无过!有时你可能会觉得你已经把你的 Git 仓库搞砸了,以至于你想完全删除它。
git reflog
是一个非常有用的命令,可以用来显示你做过的所有操作的日志信息!这包括合并、重置、回退:基本上涵盖了对一个分支的任何变更。
如果你不小心犯了错,你可以基于 reflog
提供给你的信息,通过重置 HEAD
指向轻易地撤销你的错误做法。
假使我们并不想合并源端分支,当我们执行 git reflog
后可以看到合并之前仓库的状态处于 HEAD@{1}
的位置,那就让我们执行 git reset
来将 HEAD
重新指向 HEAD@{1}
!
可以看到最新执行的动作已经反映到 reflog
的输出中了。
Git 有如此众多的上层和底层命令,我真希望能一一介绍😄 我知道还有许多其他的命令或变体,现在没有时间来介绍 - 请告诉我你最喜欢或觉得最有用的命令是什么,我可能会在另一篇文章中介绍它们!
一如既往,欢迎随时联系我!😊