完成博客 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 中被津津乐道的话题之一,正如其名“重新基于”,它能做的事情远多于合并分支。所谓“合并分支”只是把分叉点重新基于被合并分支的 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 会按此脚本逐一执行对每一个节点的操作,重新生成当前状态。更改节点的顺序或者移除某个节点将会影响最终结果,牵一发动全身,需要慎之又慎。
# 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 [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 会停止当前命令,把状态保存在本地,等待用户处理完成后通知它继续向下执行。
# 查看当前状态,如果处于 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 作基础工具使用。