其实学习 Git 版本控管的指令操作并不难,但要弄清楚 Git 到底对我的储存库做了什麽事,还真不太容易。当你一步步了解 Git 的核心与运作原理,自然能有效掌控 Git 储存库中的版本变化。本篇文章,就来说说 Git 如何记录我们的每一版的变更轨迹。
在清楚理解 Git 基础原理与物件结构之前,你不可能了解版本纪录的过程。而当你不了解版本纪录的过程,自然会担心「到底我的版本到哪去了」,也许有人跟你说「我们用了版本控管,所以所有版本都会留下,你大可放心改 Code」。知道是一回事,知不知道怎麽做又是一回事,然后是不是真的做得到又是另外一回事。我们在版控的过程中尽情 commit 建立版本,但如果有一天发现有某个版本改坏了,或是因为执行了一些合併或重置等动作导致版本消失了,那又该怎麽办呢?
还好在 Git 裡面,有一套严谨的纪录机制,而且这套机制非常开放,纪录的档案都是文字格式,还蛮容易了解,接下来我们就来说明版本纪录的过程。
我们先进入任何一个 Git 工作目录的 .git/
资料夹,你可以看到一个 logs
目录,如下图示:
这个 logs
资料夹下有个 HEAD
档案,这档案纪录著「当前分支」的版本变更纪录:
我们开启该档看看其内容 (其中物件 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
则是先显示「最新版」最后才是「第一版」:
这时我们试图建立一个新版本,看看记录档的变化,你会发现版本被建立成功:
从上图你可以发现到,这裡有个特殊的「参考名称」为 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 纪录呢?以下几个动作你可以参考,但其实可以不用死记,记住原则就好了:
除此之外,每一个分支、每一个暂存版本(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
来修正这两个过期预设值。如果你的硬碟很大,永远不想删除纪录,可以考虑设定如下:
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
重新整理或清除那些找不到、无法追踪的版本。如下图示:
Git 的版本日志(reflog)帮我们记忆在版控过程中的所有变更,帮助我们「回忆」到底这段时间到底对 Git 储存库做了什麽事。不过你也要很清楚的知道,这些只是个「日志」而已,不管有没有这些日志,都不影响我们 Git 储存库中的任何版本资讯。
我重新整理一下本日学到的 Git 指令与参数: