30 天精通 Git 版本控管

 主页   资讯   文章   代码   电子书 

第 16 天:善用版本日志 git reflog 追踪变更轨迹

其实学习 Git 版本控管的指令操作并不难,但要弄清楚 Git 到底对我的储存库做了什麽事,还真不太容易。当你一步步了解 Git 的核心与运作原理,自然能有效掌控 Git 储存库中的版本变化。本篇文章,就来说说 Git 如何记录我们的每一版的变更轨迹。

了解版本纪录的过程

在清楚理解 Git 基础原理与物件结构之前,你不可能了解版本纪录的过程。而当你不了解版本纪录的过程,自然会担心「到底我的版本到哪去了」,也许有人跟你说「我们用了版本控管,所以所有版本都会留下,你大可放心改 Code」。知道是一回事,知不知道怎麽做又是一回事,然后是不是真的做得到又是另外一回事。我们在版控的过程中尽情 commit 建立版本,但如果有一天发现有某个版本改坏了,或是因为执行了一些合併或重置等动作导致版本消失了,那又该怎麽办呢?

还好在 Git 裡面,有一套严谨的纪录机制,而且这套机制非常开放,纪录的档案都是文字格式,还蛮容易了解,接下来我们就来说明版本纪录的过程。

我们先进入任何一个 Git 工作目录的 .git/ 资料夹,你可以看到一个 logs 目录,如下图示:

image

这个 logs 资料夹下有个 HEAD 档案,这档案纪录著「当前分支」的版本变更纪录:

image

我们开启该档看看其内容 (其中物件 id 的部分我有刻意稍作删减,以免每行的内容过长):

0000000 f5685e0 Will <xxxx@gmail.com> 1381718394 +0800  commit (initial): Initial commit
f5685e0 38d924f Will <xxxx@gmail.com> 1381718395 +0800  commit: a.txt: set 1 as content
38d924f efa1e0c Will <xxxx@gmail.com> 1381734238 +0800  commit: test
efa1e0c af493e5 Will <xxxx@gmail.com> 1381837967 +0800  commit: Add c.txt

从这裡你将可看出目前在这个分支下曾经记录过 4 个版本,此时我们用 git reflog 即可列印出所有「历史纪录」的版本变化,你会发现内容是一样的,但顺序正好颠倒。从文字档中看到的内容,「第一版」在最上面,而透过 git reflog 则是先显示「最新版」最后才是「第一版」:

image

这时我们试图建立一个新版本,看看记录档的变化,你会发现版本被建立成功:

image

从上图你可以发现到,这裡有个特殊的「参考名称」为 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 的这个动作,如下图示:

image

这代表甚麽意义呢?这代表你在执行任意 Git 命令时,再也不用担心害怕你的任何资料会遗失,就算你怎样下错指令都没关系,所有已经在版本库中的档案,全部都会保存下来,完全不会有遗失的机会。所以,这时如果你想复原刚刚执行的 git reset "HEAD@{1}" --hard 动作,只要再执行一次 git reset "HEAD@{1}" --hard 即可,是不是非常棒呢!你看下图,我把刚刚的 9967b3f 这版本给救回来了:

image

纪录版本变更的原则

事实上在使用 Git 版控的过程中,有很多机会会产生「版本历史纪录」,我说的并不是单纯的 git log 显示版本纪录,而是原始且完整的变更历史纪录。这些纪录版本变更有个基本原则:【只要你透过指令修改了任何参照(ref)的内容,或是变更任何分支的 HEAD 参照内容,就会建立历史纪录】。也因为这个原则,所以指令名称才会称为 reflog,因为是改了 ref (参照内容) 才引发的 log (纪录)。

例如我们拿 git checkout 命令还切换不同的分支,这个切换的过程由于会修改 .git\HEAD 参照的内容,所以也会产生一个历史纪录,如下图示:

image

还有哪些动作会导致产生新的 reflog 纪录呢?以下几个动作你可以参考,但其实可以不用死记,记住原则就好了:

  • commit
  • checkout
  • pull
  • push
  • merge
  • reset
  • clone
  • branch
  • rebase
  • stash

除此之外,每一个分支、每一个暂存版本(stash),都会有自己的 reflog 历史纪录,这些资料也全都会储存在 .git\logs\refs\ 资料夹下。

只显示特定分支的 reflog 纪录

在查询历史纪录时,你也可以针对特定分支(Branch)进行查询,仅显示特定分支的变更历史纪录,如下图示:

image

显示 reflog 的详细版本记录

我们已经学会用 git reflog 就可以取出版本历史纪录的摘要资讯。但如果我们想要显示每一个 reflog 版本中,每一个版本的完整 commit 内容,那麽你可以用 git log -g 指令显示出来:

image

删除特定几个版本的历史纪录

基本上,版本日志(reflog)所记录的只是变更的历程,而且预设只会储存在「工作目录」下的 .git/ 目录裡,这裡所记录的一样只是 commit 物件的指标而已。无论你对这些纪录做任何操作,不管是窜改、删除,都不会影响到目前物件储存库的任何内容,也不会影响版本控管的任何资讯。

如果你想删除之前纪录的某些纪录,可以利用 git reflog delete ref@{specifier} 来删除特定历史纪录。如下图示:

image

:这些版本日志预设并不会被同步到「远端储存库」,以免多人开发时大家互相影响,所以版本日志算是比较个人的东西。

设定历史纪录的过期时间

当你的 Git 储存库越用越久,可想见这份历史纪录将会越累积越多,这样难道不会爆掉吗?还好,预设来说 Git 会帮你保存这些历史纪录 90 天,如果这些纪录中已经有些 commit 物件不在分支线上,则预设会保留 30 天。

举个例子来说,假如你先前建立了一个分支,然后 commit 了几个版本,最后直接把该分支删掉,这时这些曾经 commit 过的版本 (即 commit 物件) 还会储存在物件储存区 (object storage) 中,但已经无法使用 git log 取得该版本,我们会称这些版本为「不在分支线上的版本」。

如果你想修改预设的过期期限,可以透过 git config gc.reflogExpiregit config gc.reflogExpireUnreachable 来修正这两个过期预设值。如果你的硬碟很大,永远不想删除纪录,可以考虑设定如下:

git config --global gc.reflogExpire "never"
git config --global gc.reflogExpireUnreachable "never"

如果只想保存 7 天,则可考虑设定如下:

git config --global gc.reflogExpire "7 days"
git config --global gc.reflogExpireUnreachable "7 days"

:从上述范例所看到的 7 days 这段字,我找了好久都没有看到完整的说明文件,最后终于找到 Git 处理日期格式的原始码(C语言),有兴趣的也可以看看:http://git.kernel.org/cgit/git/git.git/tree/date.c

除此之外,你也可以针对特定分支设定其预设的过期时间。例如我想让 master 分支只保留 14 天期,而 develop 分支可以保留完整记录,那麽你可以这样设定:(注意: 以下范例我把设定储存在本地储存库中,所以使用了 --local 参数)

git config --local gc.master.reflogExpire "14 days"
git config --local gc.master.reflogExpireUnreachable "14 days"

git config --local gc.develop.reflogExpire "never"
git config --local gc.develop.reflogExpireUnreachable "never"

上述指令写入到 .git\config 的内容将会是:

[gc "master"]
    reflogExpire = 14 days
    reflogExpireUnreachable = 14 days
[gc "develop"]
    reflogExpire = never
    reflogExpireUnreachable = never

清除历史纪录

若要立即清除所有历史纪录,可以使用 git reflog expire --expire=now --all 指令完成删除工作,最后搭配 git gc 重新整理或清除那些找不到、无法追踪的版本。如下图示:

image

今日小结

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"

参考链接