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

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

## 了解版本记录的过程

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

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

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

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-8f4caeb6e4254755906a7525ad8b758f3780eb3a%2F01.png?alt=media)

这个 `logs` 资料夹下有个 `HEAD` 文件，这文件记录着「当前分支」的版本变更记录：

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-92fe27913c97d4026e0ba46c5ef9a582dcb6bbf0%2F02.png?alt=media)

我们开启该档看看其内容 (其中物件 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](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-70ffc1183f48abff4ce83f69de3b3fce52afffb2%2F03.png?alt=media)

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

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-142ba9b186a381a15f99993ad6a93633c30c49fa%2F04.png?alt=media)

从上图你可以发现到，这里有个特殊的「参考名称」为 `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](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-a9451553729da02f2ae8c103a22f481d4129b7cc%2F05.png?alt=media)

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

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-661410411d140c09458c1322a8ce630c6216c399%2F06.png?alt=media)

## 记录版本变更的原则

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

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

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-44e467005b11da4ab5635b672b1cbdb678a552cb%2F07.png?alt=media)

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

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

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

## 只显示特定分支的 reflog 记录

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

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-d0ff679482cbb56facfda9d82a16d6b383439b33%2F08.png?alt=media)

## 显示 reflog 的详细版本记录

我们已经学会用 `git reflog` 就可以取出版本历史记录的摘要信息。但如果我们想要显示每一个 reflog 版本中，每一个版本的完整 commit 内容，那么你可以用 `git log -g` 指令显示出来：

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-c51b359417cb941e1b040f3a6f23cb5ff846a843%2F09.png?alt=media)

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

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

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

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-25ea06fcd1be3b761b9af67666c6d169cab615ca%2F10.png?alt=media)

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

## 设定历史记录的过期时间

当你的 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` 重新整理或清除那些找不到、无法追踪的版本。如下图示：

![image](https://1991450022-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fj2h2C95aFkfa08k9hVd3%2Fuploads%2Fgit-blob-2de3d862875265ec6433225cf4dc1ff21e1caeb9%2F11.png?alt=media)

## 今日小结

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"

## 参考连结

* [git-reflog(1) Manual Page](http://git-scm.com/docs/git-reflog)
* [git-gc(1) Manual Page](http://git-scm.com/docs/git-gc)
* <http://git.kernel.org/cgit/git/git.git/tree/date.c>

***

* [HOME](https://kerryhuangs-organization.gitbook.io/kerry-de-bi-ji-ben/git/30-tian-jing-tong-git-ban-ben-kong-guan)
* [回目录](https://kerryhuangs-organization.gitbook.io/kerry-de-bi-ji-ben/git/30-tian-jing-tong-git-ban-ben-kong-guan/zh-cn)
* [前一天：标签 - 标记版本控制过程中的重要事件](https://kerryhuangs-organization.gitbook.io/kerry-de-bi-ji-ben/git/30-tian-jing-tong-git-ban-ben-kong-guan/zh-cn/15)
* [下一天：关于合并的基本观念与使用方式](https://kerryhuangs-organization.gitbook.io/kerry-de-bi-ji-ben/git/30-tian-jing-tong-git-ban-ben-kong-guan/zh-cn/17)

***
