完成博客Rebase,拥抱开源

立泉

博客里的第一篇文章日期是2016年,算来已有7年,刚开始只是单纯的HTML页面,后来开始用JekyllMarkdown,再后来引入WebpackReact,从Materialize切换到如今的Material Design Components。这之间的每一步都是在接触新“玩具”后对旧工程的梳理重构,同时也在Git时间线上留下了近千条Commit记录,而作为开源项目这些记录却一直让我耿耿于怀…

早期并没有意识到Commit记录在版本控制中的重要性,所以十分随意,充斥着“更新”“常规更新”之类毫无信息价值的Message,由它们填充的早期时间线看起来就乱糟糟的,一点也不“体面”。

梳理时间线势在必行,只是GitRebase和那千条记录确实令人望而生畏,前者的“畏”在于不熟悉,后者的“畏”则在于数量。后来的实际操作也证明我的担心是合理的,整整用2天时间才把这条时间线“捋顺”。其实做完之后倒觉得不过如此而已,不熟悉的Rebase变成了熟悉的Rebase,在GitHub上翻看历史记录也终于不再“尴尬”。以此为契机同时更新更详尽的README,它是我的第一个Cyber Child,也是我拥抱开源的第一个尝试,应该以更规范的标准来维护。

Rebase

RebaseMerge在合并分支时的区别一直是Git中最被人津津乐道的话题之一,但正如它的名字Rebase,“重新基于”,它能做的事远多于合并分支这样的小操作。所谓合并分支也只是把分叉点重新基于被合并分支的HEAD上,只是它能实现的功能之一部分。

Rebase的核心是Interactive模式,使用git rebase -i [commit]进入此模式后,Git会从指定Commit的下一个节点开始逐一pick并重新Commit。执行时,每一个节点都可以被修改、合并、删除,之前合并过的分支也会在这个过程中被pickCommit到当前分支上,所以执行一遍后所有分支都会被合并,当前分支存在的分叉、交叉也会消失,变成一条单一时间线,非常适合用来整理Git仓库的提交记录。

要注意的一点是,Git的每一次Commit提交,记录的都是相对于上一次Commit的文件变化,而不是完整的文件状态,那会导致仓库体积急速膨胀。因为记录的是每一次的“变化”,所以对Git来说从第一个节点开始逐一应用这些变化得到的就是当前Commit状态。在由Commit节点组成的时间线上穿梭其实就是按顺序应用、撤销这些“变化”的过程,因此用pick这个词是十分形象的,摘取变化,然后应用。

进入Interactive模式时Git会用默认编辑器打开一个脚本文件,里面是按时间顺序排列的所有要处理的Commit节点,用户以自己需求修改后Git就会按此脚本逐一执行对每一个节点的操作,重新生成当前状态。所以如果更改节点的顺序或者移除某个节点将会影响最后的结果,牵一发而动全身,需要慎之又慎。

# This is a comment

# This is my blog
reword 6275e82 My Blog
# Create CNAME to use custom domain
pick 69802dc Create CNAME
# Upload essays
pick 1328b29 My Blog
squash 0603d9a 更新
squash cc7f0aa 更新
squash ab9f1cb 更新
pick 33a957d Update README.md

Pick指的是该节点不做修改,按原本信息Commit

Reword是执行到该节点时要求用户输入新的Commit message

Squash正如其名,是压缩、合并节点。从Squash的前一个Pick节点开始,把之后连续Squash节点的变化信息都压缩到该序列的最后一个Squash节点,只做一次Commit提交,并输入新的Commit message

时间戳

每一个Commit都有2个时间戳,Author dateCommit date,前者是该节点创建的时间,后者是该节点最新一次提交的时间,包括GitHub在内的Git客户端展示的都是Commit date。而Rebase会更新Commit date时间戳,这意味着Rebase之后在GitHub上看到的所有Commit时间都会变成统一的Rebase时间,这显然是无法接受的。

解决方法是在Rebase命令中添加--committer-date-is-author-date,直到2020q4之后的Git版本它才能和-i一起配合使用,在此之前只能分别执行2次Rebase

# 进入Interactive模式
# 同时将本次Rebase的所有节点的commit date都设置为其author date
git rebase --committer-date-is-author-date -i [commit]
# 也可以单独使用
# 将指定节点之后的所有节点的commit date都设置为其author date
git rebase --committer-date-is-author-date [commit]

谨慎使用IDE

理解Rebase后就要开始对这近千条Commit节点“下手”了,JetBrainsIDE里都集成了对Rebase的图形化支持,比Git单调的编辑器更直观高效,能看到每个节点的具体修改信息,知道节点做了什么对整理时间线是很有帮助的。

JetBrains rebase

不过可能是因为我要处理的节点太多,用它尝试2次均以失败告终,尤其第一次,在花费几个小时逐条梳理完所有节点后满怀期待的点击Start Rebasing…结果却立刻弹出了一个我已记不起内容的失败弹窗…下面只有一个Retry按钮…那就Retry吧…点击后映入眼帘的是一个熟悉的、崭新的Rebasing Commits编辑窗口…之前数小时的成果瞬间归零。

第二次我尝试从中间开始Rebase,倒是执行起来了,但几秒钟后提示IDEGit因连接过多而中断,然后就要我逐个Commit自中断点之后的所有节点…可是我整理好的节点记录都在刚刚消失的编辑窗口里,现在哪有针对节点的Commit信息额…

所以,如果要处理的节点很多,还是谨慎使用IDE的图形化功能,它可能提供了更好的编辑节点的方式,但同时也会引入原本Git不存在的失败可能🙄。

手工编辑器

不使用IDE就必须手动编辑Git提供的Rebase脚本,鉴于脚本的长度和我本人对使用Vim做复杂编辑的排斥,我先把Git的默认编辑器改为了VS Code

弹出脚本后可以把内容复制到单独的文件里参照IDE的图形化工具慢慢整理,在每个要修改的节点上添加新的Commit message注释,这样Rebase时只需对照这个文件把新的Message复制粘贴过去即可。

VSCode rebase

不必恐惧

我很理解新手对Git执行复杂操作的恐惧,尤其Rebase,几条命令牵一发动全身。如果有冲突中断怎么办?如果执行到一半心态崩溃怎么办?要是想做的事没做成还把工程弄的一团糟可是得不偿失的…

其实如果清楚Rebase的原理和Git的恢复机制以及产生文件冲突的原因就不会那么恐惧了,再者,Git的状态是保存在本地.git目录中的,并不会随Terminal的关闭或者电脑关机而消失。如果出现中断,Git会停止当前命令,把状态保存在本地,等待用户处理完成后,通知它继续向下执行。

# 查看git当前状态,如果处于Rebase的中断状态,会有显示
# 且此时是无法执行其它命令的,除非处理好问题或退出Rebase
git status
# 在Rebase中断状态下执行abort可以退回到rebase之前的状态
git rebase --abort

通过ReflogReset可以很方便的恢复状态,只需在执行Rebase前记录一下当前的状态id,这样无论之后遇到什么问题,都可以轻松的恢复过来,重新尝试Rebase。或者也可以把当前工程连同其.git目录多复制几份,Rebase失败就删掉该备份,用另一个备份重新尝试,这也是Git分布式特性的一种用法😶。

关于文件冲突,其实就是分支合并时两个分支对同一文件同一位置记录的变化不一致,Git不知道要Stage哪个版本,所以会在Commit之前停下来让用户决定。处理好冲突后,Add一下这些文件,通知Git记录当前版本,再手动Commit提交,或者在Rebase中按提示git rebase --continue通知Rebase冲突解决,继续执行后续脚本命令。

理解Rebase,文字总是枯燥,最好的方法就是建一个临时仓库多多尝试,结合ReflogReset于时间线中跳跃,试几次就知道怎么回事了。而且一通百通,理解后才能安心把Git作为基础工具来用。

arrow_upward