完成博客 Rebase,拥抱开源

GitHub Breach CleanUp Commit Timestamp true 精选

博客第一篇文章日期是 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核心是 Interactive 模式,使用git rebase -i [commitID]进入此模式后,Git 会从指定commit的下一个节点开始逐一pick并重新commit。执行时每个节点都可以被修改、合并、删除,之前合并过的分支节点会在这个过程中被pick并commit到当前分支上,执行一遍后当前分支视图中存在的分叉、交叉随之消失,变成一条单一时间线,非常适合用来整理 Git 仓库的提交记录。

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

# Init 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节点,只做一次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 [commitID]
# 也可单独使用
# 将指定节点之后的所有节点的 commit date 都设置为其 author date
git rebase --committer-date-is-author-date [commitID]

另一方面,虽然 Github 展示的分支时间线采用commit date,个人主页的贡献图绿格子展示的则是author date,所以对旧工程rebase不会影响贡献图。

谨慎使用 IDE

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

JetBrains rebase

不过可能是因为要处理的节点太多,我用它尝试 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复制粘贴过去即可。

VSCode rebase

不必恐惧

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

其实如果清楚rebase的原理和 Git 的恢复机制以及产生文件冲突的原因就不会那么恐惧,再者 Git 状态是保存在本地.git/目录中的,不会随 Terminal 的关闭或者电脑关机而消失。如果出现中断,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文字总是枯燥,最好的方法是建一个临时仓库多做尝试,结合reflog和reset在时间线中跳跃,试几次就知道怎么回事了。而且一通百通,理解后才能安心把 Git 作基础工具使用。