完成博客Rebase,拥抱开源
立泉博客里的第一篇文章日期是2016年,算来已有7年,刚开始只是单纯的HTML
页面,后来开始用Jekyll
和Markdown
,再后来引入Webpack
和React
,从Materialize切换到如今的Material Design Components。这之间的每一步都是在接触新“玩具”后对旧工程的梳理重构,同时也在Git
时间线上留下了近千条Commit
记录,而作为开源项目这些记录却一直让我耿耿于怀…
早期并没有意识到Commit
记录在版本控制中的重要性,所以十分随意,充斥着“更新”“常规更新”之类毫无信息价值的Message
,由它们填充的早期时间线看起来就乱糟糟的,一点也不“体面”。
梳理时间线势在必行,只是Git
的Rebase
和那千条记录确实令人望而生畏,前者的“畏”在于不熟悉,后者的“畏”则在于数量。后来的实际操作也证明我的担心是合理的,整整用2天时间才把这条时间线“捋顺”。其实做完之后倒觉得不过如此而已,不熟悉的Rebase
变成了熟悉的Rebase
,在GitHub
上翻看历史记录也终于不再“尴尬”。以此为契机同时更新更详尽的README,它是我的第一个Cyber Child
,也是我拥抱开源的第一个尝试,应该以更规范的标准来维护。
Rebase
Rebase
和Merge
在合并分支时的区别一直是Git
中最被人津津乐道的话题之一,但正如它的名字Rebase
,“重新基于”,它能做的事远多于合并分支这样的小操作。所谓合并分支也只是把分叉点重新基于被合并分支的HEAD
上,只是它能实现的功能之一部分。
Rebase
的核心是Interactive
模式,使用git rebase -i [commit]
进入此模式后,Git
会从指定Commit
的下一个节点开始逐一pick
并重新Commit
。执行时,每一个节点都可以被修改、合并、删除,之前合并过的分支也会在这个过程中被pick
并Commit
到当前分支上,所以执行一遍后所有分支都会被合并,当前分支存在的分叉、交叉也会消失,变成一条单一时间线,非常适合用来整理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 posts
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 date
和Commit 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
节点“下手”了,JetBrains
的IDE
里都集成了对Rebase
的图形化支持,比Git
单调的编辑器更直观高效,能看到每个节点的具体修改信息,知道节点做了什么对整理时间线是很有帮助的。
不过可能是因为我要处理的节点太多,用它尝试2次均以失败告终,尤其第一次,在花费几个小时逐条梳理完所有节点后满怀期待的点击Start Rebasing
…结果却立刻弹出了一个我已记不起内容的失败弹窗…下面只有一个Retry
按钮…那就Retry
吧…点击后映入眼帘的是一个熟悉的、崭新的Rebasing Commits
编辑窗口…之前数小时的成果瞬间归零。
第二次我尝试从中间开始Rebase
,倒是执行起来了,但几秒钟后提示IDE
与Git
因连接过多而中断,然后就要我逐个Commit
自中断点之后的所有节点…可是我整理好的节点记录都在刚刚消失的编辑窗口里,现在哪有针对节点的Commit
信息额…
所以,如果要处理的节点很多,还是谨慎使用IDE
的图形化功能,它可能提供了更好的编辑节点的方式,但同时也会引入原本Git
不存在的失败可能🙄。
手工编辑器
不使用IDE
就必须手动编辑Git
提供的Rebase
脚本,鉴于脚本的长度和我本人对使用Vim
做复杂编辑的排斥,我先把Git
的默认编辑器改为了VS Code
…
弹出脚本后可以把内容复制到单独的文件里参照IDE
的图形化工具慢慢整理,在每个要修改的节点上添加新的Commit message
注释,这样Rebase
时只需对照这个文件把新的Message
复制粘贴过去即可。
不必恐惧
我很理解新手对Git
执行复杂操作的恐惧,尤其Rebase
,几条命令牵一发动全身。如果有冲突中断怎么办?如果执行到一半心态崩溃怎么办?要是想做的事没做成还把工程弄的一团糟可是得不偿失的…
其实如果清楚Rebase
的原理和Git
的恢复机制以及产生文件冲突的原因就不会那么恐惧了,再者,Git
的状态是保存在本地.git
目录中的,并不会随Terminal
的关闭或者电脑关机而消失。如果出现中断,Git
会停止当前命令,把状态保存在本地,等待用户处理完成后,通知它继续向下执行。
# 查看git当前状态,如果处于Rebase的中断状态,会有显示
# 且此时是无法执行其它命令的,除非处理好问题或退出Rebase
git status
# 在Rebase中断状态下执行abort可以退回到rebase之前的状态
git rebase --abort
通过Reflog
和Reset
可以很方便的恢复状态,只需在执行Rebase
前记录一下当前的状态id
,这样无论之后遇到什么问题,都可以轻松的恢复过来,重新尝试Rebase
。或者也可以把当前工程连同其.git
目录多复制几份,Rebase
失败就删掉该备份,用另一个备份重新尝试,这也是Git分布式
特性的一种用法😶。
关于文件冲突,其实就是分支合并时两个分支对同一文件同一位置记录的变化不一致,Git
不知道要Stage
哪个版本,所以会在Commit
之前停下来让用户决定。处理好冲突后,Add
一下这些文件,通知Git
记录当前版本,再手动Commit
提交,或者在Rebase
中按提示git rebase --continue
通知Rebase
冲突解决,继续执行后续脚本命令。
理解Rebase
,文字总是枯燥,最好的方法就是建一个临时仓库多多尝试,结合Reflog
和Reset
于时间线中跳跃,试几次就知道怎么回事了。而且一通百通,理解后才能安心把Git
作为基础工具来用。