本期精读的文章是：[When You “Git” in Trouble - a Version Control Story](https://hackernoon.com/when-you-git-in-trouble-a-version-control-story-97e6421b5c0e)

# 1 引言

git 作为目前最流行的版本控制系统，它拥有众多的用户并管理着数量庞大的实际软件项目。

本文主要通过一个实际的例子来描述，当项目（代码）仓库出现问题时如何使用 git 进行有效的维护，并分享一些 git 使用经验以及分析 git 的内部实现机制。

# 2 内容概要

我们在管理项目代码仓库时，经常会碰到一些棘手的问题，比如：在使用 git 的过程中，有时会不小心丢失 commit 信息。如果实际场景中发生了类似的问题，该如何使用 git 找回丢失的 commit 呢？

首先，在 git 中想要找回丢失的 commit，就需要找出那些 commit 的 SHA，然后添加一个指向它的分支。

由于 git 会记录下每次修改 HEAD 的操作，当执行提交或修改分支的命令时 reflog 就会更新（执行 git update-ref 命令也可以更新 reflog），因此可以执行 git reflog 命令来查看当前的状态。

```js
$ git reflog

1a410ef HEAD@{0}: 1a410efbd13591db07496601ebc7a059dd55cfe9: updating HEAD
ab1afef HEAD@{1}: ab1afef80fac8e34258ff41fc1b867c702daa24b: updating HEAD
```

执行 git log -g 命令可以查看更加详细 reflog 的日志。

```js
$ git log -g

commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0}
Reflog message: updating HEAD

    third commit

commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1}
Reflog message: updating HEAD

    modified repo a bit
```

确定丢失的 commit 后，就可以在这个 commit 上创建一个新分支将其恢复过来。比如，在 commit (ab1afef) 上创建 new-branch 分支，即可找回丢失的 commit 数据。

```js
$ git branch new-branch ab1afef

$ git log --pretty=oneline new-branch

ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
```

如果引起 commit 丢失的原因并没有记录在 reflog 中，即没有在 .git/logs/ 中（因为 reflog 数据是保存在 .git/logs/ 目录下的），这样就会导致丢失的 commit 不会被任何东西引用。这种情况应该如何恢复 commit 数据呢？

这里可以执行 git fsck --full 命令，该命令会检查仓库的数据完整性，会显示所有未被其他对象引用的所有对象。

```js
$ git fsck --full

dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293
```

这样就可以从 dangling commit 中找到丢失的 commit 信息，确定丢失的 commit 后，就可以在这个 commit 上创建一个新分支将其恢复过来。

至此，文章分享了一个在实际工作中维护项目仓库时经常会遇到的难题的解决思路和方法。

# 3 精读

上述文章中执行 git fsck 命令时出现了 blob、tree、commit 等关键词，虽然我们经常会看到此类的关键词，但可能并不清楚其真正含义，因此，下面内容将从 blob、tree、commit 这三个内部对象去深入分析 git 内部数据结构管理的机制。

git 的版本控制实际就是对文件进行管理和控制，其管理方法就是为每个文件生成（key, object）的结构，并利用 sha-1 加密算法，对每一个文件生成唯一的字符序列作为 hash_key，且文件改变就会生成新的 （key, object）。

执行 git init 初始化一个本地仓库，查看隐藏目录 .git 中的目录结构。其中，objects 目录下只有 info 和 pack 两个空文件夹，没有任何其他文件被记录下来。

![](https://user-images.githubusercontent.com/3983192/34076511-7b2db15a-e324-11e7-8e7f-e80ebdbb6e44.png)

## blob 对象

在当前项目仓库中添加文件 file1.js，执行 git hash-object file1.js，生成一个 40 字符长度的 hash-key 序列：08219db9b0969fa29cf16fd04df4a63964da0b69。

执行 git add file_1.txt，objects 中多了一个 08 对象。

![](https://user-images.githubusercontent.com/3983192/34076544-9e24bb62-e325-11e7-8a82-c71577d9589f.png)

其实是 40 位 hash-key 的前两位作为目录名，后 38 位 作为文件名，这个对象里面的内容就是 file1.js 的内容，可以查看该对象的内容和类型。

执行 git cat-file -p [hash-key] 可以查看已经存在的 object 对象内容；

执行 git cat-file -t [hash-key] 可以查看已经存在的 object 对象类型；

![](https://user-images.githubusercontent.com/3983192/34076684-6e7c5f74-e329-11e7-91e6-8fdf0a250c32.png)

git object 有四种类型，第一种类型 blob，用来储存文件内容。

## tree 对象

blob 对象用于存储对应文件的内容，tree 对象可以理解为存储目录，其树节点信息包含：文件名，hash-key，文件类型、权限等，这样就可以组织整个需要控制文件的结构。

在当前项目仓库中添加文件夹 dir1，在 dir1 中添加文件 file2.js。执行 git add 将内容加入到暂存区，执行 git hash-object dir1/file2.js 查看生成的 hash-key：30d67d4672d5c05833b7192cc77a79eaafb5c7ad。
 
 ![](https://user-images.githubusercontent.com/3983192/34077156-cac98c2c-e336-11e7-80ff-e636a635c214.png)
 
查看 objects 目录，只新增了一个 30 目录，即 30d67d4672d5c05833b7192cc77a79eaafb5c7ad 对应的 file2.js 文件。

![](https://user-images.githubusercontent.com/3983192/34077176-a7da8d28-e337-11e7-855c-3e77f6c610f7.png)

说明 file2.js 文件生成了 hash-key，但 dir1 目录并没有生成 tree 对象，tree 对象是在 commit 的过程中生成的，其生成会根据 .git 目录下的 index 文件的内容来创建。git add 的操作就是将文件的信息保存到 index 文件中，在 commit 时，根据 index 的内容来生成 tree 对象。

执行 git ls-files --stage 命令，可以看到 index 中包含了创建 tree 对象的信息：文件类型（100644）、hash-key、目录结构以及文件名。

![](https://user-images.githubusercontent.com/3983192/34077197-718376da-e338-11e7-9b73-bc437b9b4f83.png)

进行一次 commit，生成 commit 对象和 tree 对象。其中，master^{tree} 表示 master 分支所指向的 tree 对象。

该 tree 对象是当前对应的目录，目录下有一个名为 dir1 的 tree 对象和 file1.js 的 blob 对象。

![](https://user-images.githubusercontent.com/3983192/34077217-3201a1de-e339-11e7-87dc-f7b3b358ecb5.png)

查看 dir1 对应的 tree 对象的内容，该 tree 对象只包含 file2.js 的信息。

![](https://user-images.githubusercontent.com/3983192/34077239-d081bd12-e339-11e7-941c-00f296936f05.png)

当前项目的 git 仓库内部结构图如下：

![](https://user-images.githubusercontent.com/3983192/34077284-39dc23c8-e33b-11e7-86d7-794fa51d11c9.png)

## commit 对象

只有在执行 git commit 时，才会根据 index 记录的内容生成 tree 对象，则 commit 对象中会有两个内容：

- 代表项目目录的 tree 对象的 key
- 上一个 commit 的 key

项目中 objects 目录的内容：

![](https://user-images.githubusercontent.com/3983192/34078157-79aeaa84-e34f-11e7-93d9-49d807daa3f9.png)

目前每个目录里有一个对象，共 5 个对象，之前的总体 tree 图只包含了 4 个对象，执行 git log 查看 commit 记录。

![](https://user-images.githubusercontent.com/3983192/34078169-ed3a2db6-e34f-11e7-8a8d-f5d902584ed0.png)
 
objects 中 65 对应的文件夹里面的文件就是 commit 对象，它指向项目目录 tree 以及上一次的 commit，由于是第一个 commit，因此不存在上一个 commit。

![](https://user-images.githubusercontent.com/3983192/34078228-175d7282-e351-11e7-9292-167e4c937bc4.png)
 
commit 对象内容指向项目目录 tree，所以能获取到一个 commit，可以得到当前完整的文件状况，objects 结构图如下：

![](https://user-images.githubusercontent.com/3983192/34078312-b9a9edc6-e352-11e7-835e-63d05caefd29.png)

再新增一个目录 dir2，该目录下新增文件 file3.js，执行 git add 和 git commit 后查看新的 commit 信息。

![](https://user-images.githubusercontent.com/3983192/34078372-14331b5e-e354-11e7-83dd-717aee47af4e.png)

新的 commit 指向了上一个 commit，还指向了一个新生成的 tree，该 tree 表示了新的项目目录情况，查看该 tree 的内容。

![](https://user-images.githubusercontent.com/3983192/34078398-91b8b58e-e354-11e7-9024-5f045062d67c.png)

这个 tree 包含了当前的文件目录和内容，目前对象完整的图如下：

![](https://user-images.githubusercontent.com/3983192/34078542-59591b5e-e357-11e7-8ab5-7e2732dd354f.png)

可以看到 commit 对象指向了工作目录 tree，因此只要切换 commit，就可以随意切换对应的版本内容，当前 .git/objects 目录内容如下：

![](https://user-images.githubusercontent.com/3983192/34078587-e37f3c5a-e357-11e7-9413-da26ac5c868d.png)

git 版本控制住要就是围绕这三类内部对象展开，分别为 blob（记录文件内容），tree（目录结构），commit（工作目录 tree 以及提交历史）。

# 4 总结

本文从一个实际问题即如何使用 git 维护丢失的 commit 入手，并给出相应的解决思路和方案，以及通过 git 内部三种对象来分析其内部工作机制，希望能过解决读者们对 git 存在困惑的地方，同时也希望读者们积极参与每周精度的讨论，各抒己见，分享自身在实际工作中遇到的问题及其解决思路。

> 讨论地址是：[精读《When You “Git” in Trouble - a Version Control Story》 · Issue #49 · dt-fe/weekly](http://github.com/dt-fe/weekly/issues/49)

> 如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，每周五发布。
