第 16 天:善用版本日志 git reflog 追踪变更轨迹
Last updated
Last updated
其实学习 Git 版本控制的指令操作并不难,但要弄清楚 Git 到底对我的仓库做了什么事,还真不太容易。当你一步步了解 Git 的核心与运作原理,自然能有效掌控 Git 仓库中的版本变化。本篇文章,就来说说 Git 如何记录我们的每一版的变更轨迹。
在清楚理解 Git 基础原理与物件结构之前,你不可能了解版本记录的过程。而当你不了解版本记录的过程,自然会担心「到底我的版本到哪去了」,也许有人跟你说「我们用了版本控制,所以所有版本都会留下,你大可放心改 Code」。知道是一回事,知不知道怎么做又是一回事,然后是不是真的做得到又是另外一回事。我们在版控的过程中尽情 commit 建立版本,但如果有一天发现有某个版本改坏了,或是因为执行了一些合并或重置等动作导致版本消失了,那又该怎么办呢?
还好在 Git 里面,有一套严谨的记录机制,而且这套机制非常开放,记录的文件都是文字格式,还蛮容易了解,接下来我们就来说明版本记录的过程。
我们先进入任何一个 Git 工作目录的 .git/
资料夹,你可以看到一个 logs
目录,如下图示:
这个 logs
资料夹下有个 HEAD
文件,这文件记录着「当前分支」的版本变更记录:
我们开启该档看看其内容 (其中物件 id 的部分我有刻意稍作删減,以免每行的内容过长):
从这里你将可看出目前在这个分支下曾经记录过 4 个版本,此时我们用 git reflog
即可列印出所有「历史记录」的版本变化,你会发现内容是一样的,但顺序正好颠倒。从文字档中看到的内容,「第一版」在最上面,而通过 git reflog
则是先显示「最新版」最后才是「第一版」:
这时我们试图建立一个新版本,看看记录档的变化,你会发现版本被建立成功:
从上图你可以发现到,这里有个特殊的「参考名称」为 HEAD@{0}
,这里每个版本都会有一个历史记录都会有个编号,代表着这个版本的在记录档中的顺位。如果是 HEAD@{0}
的话,永远代表目前分支的「最新版」,换句话说就是你在这个「分支」中最近一次对 Git 操作的记录。你对 Git 所做的任何版本变更,全部都会被记录下来。
初学者刚开始使用 Git 很有可能会不小心执行错误,例如通过 git merge
执行合并时发生了冲突,或是通过 git pull
取得远端仓库最新版时发生了失误。在这种情況下,你可以利用 HEAD@{0}
这个特殊的「参考名称」来对此版本「定位」,并将目前的 Git 仓库版本回复到前一版或前面好几版。
例如,我们如果想要「取消」最近一次的版本记录,我们可以通过 git reset HEAD@{1} --hard
来复原变更。如此一来,这个原本在 HEAD@{0}
的变更,就会被删除。不过,在 Git 里面,所有的变更都会被记录,其中包含你做 git reset "HEAD@{1}" --hard
的这个动作,如下图示:
这代表什么意义呢?这代表你在执行任意 Git 命令时,再也不用担心害怕你的任何资料会遗失,就算你怎样下错指令都没关系,所有已经在版本库中的文件,全部都会保存下来,完全不会有遗失的机会。所以,这时如果你想复原刚刚执行的 git reset "HEAD@{1}" --hard
动作,只要再执行一次 git reset "HEAD@{1}" --hard
即可,是不是非常棒呢!你看下图,我把刚刚的 9967b3f
这版本给救回来了:
事实上在使用 Git 版控的过程中,有很多机会会产生「版本历史记录」,我说的并不是单纯的 git log
显示版本记录,而是原始且完整的变更历史记录。这些记录版本变更有个基本原则:【只要你通过指令修改了任何参照(ref)的内容,或是变更任何分支的 HEAD
参照内容,就会建立历史记录】。也因为这个原则,所以指令名称才会称为 reflog
,因为是改了 ref
(参照内容) 才引发的 log
(记录)。
例如我们拿 git checkout
命令还切换不同的分支,这个切换的过程由于会修改 .git\HEAD
参照的内容,所以也会产生一个历史记录,如下图示:
还有哪些动作会导致产生新的 reflog 记录呢?以下几个动作你可以参考,但其实可以不用死记,记住原则就好了:
commit
checkout
pull
push
merge
reset
clone
branch
rebase
stash
除此之外,每一个分支、每一个暂存版本(stash),都会有自己的 reflog 历史记录,这些资料也全都会储存在 .git\logs\refs\
资料夹下。
在查询历史记录时,你也可以针对特定分支(Branch)进行查询,仅显示特定分支的变更历史记录,如下图示:
我们已经学会用 git reflog
就可以取出版本历史记录的摘要信息。但如果我们想要显示每一个 reflog 版本中,每一个版本的完整 commit 内容,那么你可以用 git log -g
指令显示出来:
基本上,版本日志(reflog)所记录的只是变更的历程,而且预设只会储存在「工作目录」下的 .git/
目录里,这里所记录的一样只是 commit 物件的指标而已。无论你对这些记录做任何操作,不管是窜改、删除,都不会影响到目前物件仓库的任何内容,也不会影响版本控制的任何信息。
如果你想删除之前记录的某些记录,可以利用 git reflog delete ref@{specifier}
来删除特定历史记录。如下图示:
注:这些版本日志预设并不会被同步到「远端仓库」,以免多人开发时大家互相影响,所以版本日志算是比较个人的东西。
当你的 Git 仓库越用越久,可想见这份历史记录将会越累积越多,这样难道不会爆掉吗?还好,预设来说 Git 会帮你保存这些历史记录 90 天,如果这些记录中已经有些 commit 物件不在分支线上,则预设会保留 30 天。
举个例子来说,假如你先前建立了一个分支,然后 commit 了几个版本,最后直接把该分支删掉,这时这些曾经 commit 过的版本 (即 commit 物件) 还会储存在物件储存区 (object storage) 中,但已经无法使用 git log
取得该版本,我们会称这些版本为「不在分支线上的版本」。
如果你想修改预设的过期期限,可以通过 git config gc.reflogExpire
与 git config gc.reflogExpireUnreachable
来修正这两个过期预设值。如果你的硬盘很大,永远不想删除记录,可以考虑设定如下:
如果只想保存 7 天,则可考虑设定如下:
注:从上述范例所看到的 7 days
这段字,我找了好久都没有看到完整的说明文件,最后终于找到 Git 处理日期格式的原始码(C语言),有兴趣的也可以看看:http://git.kernel.org/cgit/git/git.git/tree/date.c
除此之外,你也可以针对特定分支设定其预设的过期时间。例如我想让 master
分支只保留 14 天期,而 develop
分支可以保留完整记录,那么你可以这样设定:(注意: 以下范例我把设定储存在本地仓库中,所以使用了 --local
参数)
上述指令写入到 .git\config
的内容将会是:
若要立即清除所有历史记录,可以使用 git reflog expire --expire=now --all
指令完成删除工作,最后搭配 git gc
重新整理或清除那些找不到、无法追踪的版本。如下图示:
Git 的版本日志(reflog)帮我们记忆在版控过程中的所有变更,帮助我们「回忆」到底这段时间到底对 Git 仓库做了什么事。不过你也要很清楚的知道,这些只是个「日志」而已,不管有没有这些日志,都不影响我们 Git 仓库中的任何版本信息。
我重新整理一下本日学到的 Git 指令与参数:
git reflog
git reflog [ref]
git log -g
git reset "HEAD@{1}" --hard
git reflog delete "ref@{specifier}"
git reflog delete "HEAD@{0}"
git reflog expire --expire=now --all
git gc
git config --global gc.reflogExpire "never"
git config --global gc.reflogExpireUnreachable "never"