1. Git 对象简介

Git 是一个以文件为核心版本控制系统。它通过三种核心对象来管理数据:blobtreecommit

  • blob:用于存储文件内容(二进制数据)。
  • tree:用于表示目录结构,关联 blob 与文件路径。
  • commit:指向某个 tree,表示一次提交的快照。

由于 Git 本身不直接跟踪目录,只有目录中存在文件时,它才会被 Git 记录。因此,直接添加一个完全空的目录到 Git 仓库是无效的


2. 示例仓库

我们以一个示例仓库来演示:

$ git clone https://github.com/f/awesome-chatgpt-prompts && cd awesome-chatgpt-prompts
$ tree
.
├── CNAME
├── _config.yml
├── CONTRIBUTING.md
├── _layouts
│   └── default.html
├── LICENSE
├── prompts.csv
├── README.md
└── scripts
    └── find-prompt

3 directories, 8 files

这个仓库中有 3 个目录和 8 个文件。其中 .git 目录是隐藏的,不显示在上面的输出中。


3. Git 对象详解

3.1 Blob

Blob 是 Git 中最小的存储单位,用于保存文件内容。Blob 本身不包含文件名或权限信息,只存储内容的二进制数据。

Blob 的格式如下:

blob <SIZE_BYTES>\0<CONTENT_BINARY>

当我们将一个新文件加入暂存区(git add)时,Git 会为该文件创建一个新的 blob 对象。


3.2 Tree

Tree 对象用于表示目录结构。它将文件名、权限等信息与对应的 blob 关联起来。

Tree 的格式如下:

tree <SIZE_BYTES>\0
<FILE_1_MODE> <1_PATH>\0<1_BLOB_HASH>
<FILE_1_MODE> <2_PATH>\0<2_BLOB_HASH>
...

例如,使用 git ls-tree 可查看当前提交的 tree 结构:

$ git ls-tree 9f94573322353b1f1ccb298c7f8383fc64a589e8
040000 tree 112461b5254d5c2929e158e20f396e8594095ab2    .github
100644 blob 3571f7ca907e841f7aa19052d8ca842175ee8f50    CNAME
...

其中:

  • 040000 表示这是一个目录(tree)
  • 100644 表示这是一个普通文件(blob)
  • 后面是对象的哈希值和文件名

3.3 Commit

Commit 是 Git 的提交对象,它指向一个 tree,表示一次提交的完整快照。

例如,我们可以用 git rev-parse 查看某个提交的 tree:

$ git rev-parse 285f36b3cb794bedc3ee98bea91455ee7deca681^{tree}
285f36b3cb794bedc3ee98bea91455ee7deca681

这说明 commit 和 tree 是一一对应的。


4. Git 与文件系统对象

文件系统中主要有两种对象:

  • 文件(file):存储数据
  • 目录(directory):存储文件列表

Git 并不直接跟踪目录,它只跟踪文件。只有当目录中存在至少一个文件时,该目录才会被 Git 记录


5. 尝试直接添加空目录

我们尝试创建一个空目录并添加到 Git:

$ mkdir void
$ git add void
$ git commit -m 'empty directory'

结果是:

nothing to commit, working tree clean

Git 会忽略空目录,不会将其加入提交。


6. 使用 git mktree 强制添加空目录

虽然不推荐,但可以通过 Git 的底层命令手动创建 tree 对象,将空目录加入 Git 元数据。

$ EMPTYDIR=void
$ emptytree=$(cat /dev/null | git mktree)
$ roottree=$(printf '040000 tree %s\t%s' $emptytree $EMPTYDIR | git mktree)

然后提交该 tree:

$ initcommit=$(git commit-tree $roottree -m 'tree with empty directory')
$ git branch branch1 $initcommit
$ git checkout branch1

确认目录已加入 Git:

$ git ls-tree HEAD
040000 tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904    void

⚠️ 这种方式虽然能在 Git 元数据中保留目录,但不会在工作区中创建实际目录,实用性有限。


7. 使用 .gitkeep 文件

这是最常见也是最推荐的做法。在空目录中添加一个 .gitkeep 文件,让 Git 识别该目录。

$ mkdir void
$ touch void/.gitkeep
$ git add void
$ git commit -m 'add empty directory with .gitkeep'

✅ 优点:

  • 是标准做法
  • 文件名语义明确
  • 隐藏文件(以 . 开头)
  • 不影响目录内容

8. 使用 .gitignore 文件

另一种方式是使用 .gitignore 文件作为占位符,同时控制目录内容。

$ mkdir void
$ touch void/.gitignore

可以编辑 .gitignore 文件内容如下:

# ignore all directory files
*
# except .gitignore
!.gitignore

✅ 优点:

  • 可以控制目录内容
  • 也是隐藏文件
  • 与 Git 语义一致

9. 使用占位文件(placeholder)

你也可以使用任意文件作为占位符,例如 placeholder.placeholder

$ mkdir void
$ touch void/placeholder
$ git add void
$ git commit -m 'add placeholder file'

你也可以使用 README.md 文件说明该目录用途。

✅ 优点:

  • 灵活,可自定义文件名
  • 适用于有特定用途的目录

10. 总结

Git 本身不直接跟踪目录,但我们可以使用以下方法“保留”空目录:

方法 是否推荐 说明
git mktree 仅用于元数据,不创建实际目录
.gitkeep 最常见、推荐做法
.gitignore 可控制目录内容
占位文件 灵活,适合有说明需求的目录

推荐做法:使用 .gitkeep.gitignore 文件,保持目录结构清晰且语义明确。


原始标题:Git Objects and How to Add an Empty Directory to Git Repository