Skip to content

Git 工作原理

1. 知识点大纲

1.1 概述

理解 Git 的工作原理是成为 Git 高手的必经之路。虽然日常使用中我们只需要掌握基本命令,但深入了解 Git 的内部机制可以帮助你:

  • 更好地理解每个命令背后的操作
  • 在遇到问题时快速定位和解决
  • 优化 Git 仓库性能
  • 实现更高级的定制需求

Git 的设计哲学是"简单而强大"。它本质上是一个内容寻址文件系统,在此基础上构建了版本控制功能。理解这一点,你会发现 Git 的很多设计都变得顺理成章。

Git 的核心设计理念:

  • 快照而非差异:Git 记录每次提交的完整快照,而非文件差异
  • 完整性校验:使用 SHA-1 哈希确保数据完整性
  • 本地优先:几乎所有操作都在本地完成
  • 数据只增不减:一旦提交,数据就安全存储在数据库中

1.2 基本概念

语法

底层命令(Plumbing Commands):

Git 命令分为高层命令(Porcelain)和底层命令(Plumbing)。底层命令更接近 Git 的内部实现。

bash
# 对象操作
git hash-object [-w] [--stdin] <file>  # 计算对象哈希,可选存储
git cat-file [-t|-p] <object>           # 查看对象类型或内容

# 索引操作
git update-index --add <file>           # 添加文件到索引
git ls-files [--stage]                   # 列出索引中的文件
git write-tree                           # 将索引写入树对象

# 引用操作
git update-ref <ref> <sha>              # 更新引用
git symbolic-ref <ref> <target>         # 更新符号引用

# 提交操作
git commit-tree <tree> [-p <parent>]    # 创建提交对象

高层命令(Porcelain Commands):

日常使用的主要命令:

bash
# 常用高层命令
git init                    # 初始化仓库
git add <file>              # 添加到暂存区
git commit -m "message"     # 提交
git status                  # 查看状态
git log                     # 查看历史
git diff                    # 查看差异
git branch                  # 分支操作
git merge                   # 合并分支
git rebase                  # 变基

语义

Git 目录结构:

bash
.git/
├── HEAD                    # 当前分支引用
├── config                  # 仓库配置
├── description             # 仓库描述(GitWeb 使用)
├── hooks/                  # 钩子脚本
   ├── pre-commit
   ├── post-commit
   └── ...
├── info/                   # 额外信息
   └── exclude             # 本地忽略规则
├── objects/                # 对象数据库
   ├── pack/               # 打包对象
   ├── pack-*.pack     # 打包文件
   └── pack-*.idx      # 索引文件
   └── info/               # 对象信息
       └── packs           # 打包文件列表
├── refs/                   # 引用
   ├── heads/              # 本地分支
   ├── remotes/            # 远程分支
   └── tags/               # 标签
├── index                   # 暂存区
└── logs/                   # 日志
    ├── HEAD                # HEAD 变更日志
    └── refs/               # 引用变更日志

对象存储路径:

Git 对象存储在 .git/objects 目录下,以 SHA-1 哈希的前两位作为子目录名:

bash
# 对象路径格式
.git/objects/<前两位哈希>/<剩余38位哈希>

# 示例
.git/objects/8d/01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a

规范

对象命名规范:

bash
# SHA-1 哈希格式
<对象类型> <内容长度>\0<内容>

# 示例:blob 对象
blob 11\0Hello, Git

# 计算哈希
$ echo -n "blob 11\0Hello, Git" | shasum
8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a

引用规范:

bash
# 引用格式
refs/<类型>/<名称>

# 示例
refs/heads/main              # 本地分支
refs/remotes/origin/main     # 远程分支
refs/tags/v1.0.0             # 标签
refs/stash                   # stash 引用

1.3 原理深度解析

Git 对象存储机制

Git 使用内容寻址存储方式,每个对象都通过其内容的 SHA-1 哈希值来标识。

对象存储流程:

┌─────────────────────────────────────────────────────────────┐
│                    Git 对象存储流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 构造对象内容                                             │
│     ┌─────────────────────────────────────────────────┐    │
│     │ "blob 11\0Hello, Git"                           │    │
│     └─────────────────────────────────────────────────┘    │
│                          │                                  │
│                          ▼                                  │
│  2. 计算 SHA-1 哈希                                         │
│     ┌─────────────────────────────────────────────────┐    │
│     │ SHA-1: 8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a │    │
│     └─────────────────────────────────────────────────┘    │
│                          │                                  │
│                          ▼                                  │
│  3. zlib 压缩                                               │
│     ┌─────────────────────────────────────────────────┐    │
│     │ 压缩后的二进制数据                                │    │
│     └─────────────────────────────────────────────────┘    │
│                          │                                  │
│                          ▼                                  │
│  4. 存储到文件系统                                           │
│     ┌─────────────────────────────────────────────────┐    │
│     │ .git/objects/8d/01495a85a9f6e2f8e9bc4b5a5a5a... │    │
│     └─────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

手动创建 Git 对象:

bash
# 创建 blob 对象
$ echo "Hello, Git" | git hash-object -w --stdin
8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a

# 查看对象类型
$ git cat-file -t 8d01495a
blob

# 查看对象内容
$ git cat-file -p 8d01495a
Hello, Git

# 查看对象大小
$ git cat-file -s 8d01495a
11

# 直接查看文件内容(zlib 压缩)
$ python3 << 'EOF'
import zlib
import binascii

with open('.git/objects/8d/01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a', 'rb') as f:
    content = f.read()
    decompressed = zlib.decompress(content)
    print(decompressed.decode('utf-8'))
EOF
blob 11Hello, Git

Git 索引(暂存区)原理

Git 索引是一个二进制文件,存储在 .git/index,记录了工作目录和仓库之间的状态信息。

索引结构:

┌─────────────────────────────────────────────────────────────┐
│                    Git 索引文件结构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 文件头(Header)                                      │   │
│  │ - 签名:DIRC                                         │   │
│  │ - 版本号                                             │   │
│  │ - 条目数量                                           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 索引条目(Index Entries)                            │   │
│  │ - 创建时间                                           │   │
│  │ - 修改时间                                           │   │
│  │ - 设备号                                             │   │
│  │ - inode 号                                           │   │
│  │ - 文件模式                                           │   │
│  │ - 用户 ID                                            │   │
│  │ - 组 ID                                              │   │
│  │ - 文件大小                                           │   │
│  │ - SHA-1 哈希                                         │   │
│  │ - 标志位                                             │   │
│  │ - 文件名                                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 文件尾校验和(Checksum)                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

索引操作示例:

bash
# 查看索引内容
$ git ls-files --stage
100644 8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a 0 README.md
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 .gitignore

# 查看索引详细信息
$ git ls-files --debug
README.md
  ctime: 1705287600:0
  mtime: 1705287600:0
  dev: 16777220
  ino: 12345678
  uid: 501
  gid: 20
  size: 11
  flags: 0

# 索引状态
$ git status
On branch main
Changes to be committed:
  modified:   README.md    # 已暂存

Changes not staged for commit:
  modified:   config.php   # 未暂存

Untracked files:
  new.php                   # 未跟踪

Git 分支实现原理

Git 的分支实现非常轻量级,本质上只是一个包含 41 字节的文件。

分支文件内容:

bash
# 查看分支文件
$ cat .git/refs/heads/main
abc1234567890abcdef1234567890abcdef12345678

# 只有 41 字节(40 字符哈希 + 换行符)
$ wc -c .git/refs/heads/main
41 .git/refs/heads/main

分支创建过程:

bash
# 创建分支的底层实现
$ git branch feature
# 等价于
$ git update-ref refs/heads/feature $(git rev-parse HEAD)

# 查看新分支
$ cat .git/refs/heads/feature
abc1234567890abcdef1234567890abcdef12345678

HEAD 引用:

bash
# HEAD 是符号引用
$ cat .git/HEAD
ref: refs/heads/main

# 分离 HEAD 状态
$ git checkout abc1234
$ cat .git/HEAD
abc1234567890abcdef1234567890abcdef12345678

# 更新 HEAD
$ git symbolic-ref HEAD refs/heads/feature
$ cat .git/HEAD
ref: refs/heads/feature

Git 提交链

每个提交都包含指向父提交的引用,形成一条提交链。

提交链结构:

┌─────────────────────────────────────────────────────────────┐
│                    Git 提交链                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Commit C (HEAD -> main)                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ tree: tree-c                                         │   │
│  │ parent: Commit B                                     │───┐
│  │ author: Zhang San                                    │   │
│  │ message: feat: 添加功能 C                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                          ▲                                  │
│                          │                                  │
│  Commit B                  │                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ tree: tree-b                                         │◄──┘
│  │ parent: Commit A                                     │───┐
│  │ author: Zhang San                                    │   │
│  │ message: feat: 添加功能 B                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                          ▲                                  │
│                          │                                  │
│  Commit A (初始提交)       │                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ tree: tree-a                                         │◄──┘
│  │ parent: none                                         │
│  │ author: Zhang San                                    │
│  │ message: feat: 初始化项目                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

遍历提交链:

bash
# 查看提交链
$ git log --oneline
abc1234 (HEAD -> main) feat: 添加功能 C
def5678 feat: 添加功能 B
ghi9012 feat: 初始化项目

# 查看提交的父提交
$ git cat-file -p HEAD
tree 4b825dc6...
parent def5678...
author Zhang San ...
...

# 查看提交的树对象
$ git ls-tree HEAD
100644 blob 8d01495a...    README.md
040000 tree 4b825dc6...    src

# 递归查看树对象
$ git ls-tree -r HEAD
100644 blob 8d01495a...    README.md
100644 blob e69de29b...    src/main.php

Git 合并原理

Git 合并分为两种类型:快进合并(Fast-forward)和三方合并(Three-way merge)。

快进合并:

当目标分支是当前分支的直接后继时,只需移动分支指针。

合并前:
main ──► A ──► B
feature ────────► C ──► D

合并后:
main ──► A ──► B ──► C ──► D
feature ────────────────────►

三方合并:

当两个分支分叉时,需要找到共同祖先进行三方合并。

合并前:
          A ──► B ──► C (main)
         /
共同祖先 ──► X
         \
          D ──► E (feature)

合并后:
          A ──► B ──► C ──┐
         /                 \
共同祖先 ──► X                ├──► M (合并提交)
         \                 /
          D ──► E ────────┘

三方合并示例:

bash
# 查看合并基
$ git merge-base main feature
xyz5678

# 执行合并
$ git merge feature
Auto-merging config.php
Merge made by the 'recursive' strategy.

# 查看合并提交
$ git show HEAD
commit abc1234...
Merge: def5678 ghi9012
Author: Zhang San <zhang@example.com>

    Merge branch 'feature'

Git 打包原理

Git 使用打包文件(packfile)优化存储和传输效率。

打包机制:

┌─────────────────────────────────────────────────────────────┐
│                    Git 打包机制                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  松散对象(Loose Objects)                                   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │
│  │ Object A │  │ Object B │  │ Object C │                  │
│  │ (完整)   │  │ (完整)   │  │ (完整)   │                  │
│  └──────────┘  └──────────┘  └──────────┘                  │
│       │              │              │                       │
│       └──────────────┼──────────────┘                       │
│                      ▼                                      │
│  打包过程                                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. 找出相似对象                                      │   │
│  │ 2. 计算差异                                          │   │
│  │ 3. 存储基础对象 + 差异                               │   │
│  └─────────────────────────────────────────────────────┘   │
│                      │                                      │
│                      ▼                                      │
│  打包文件(Packfile)                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ Object A (完整)                                      │   │
│  │ Object B = A + delta                                 │   │
│  │ Object C = A + delta                                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

打包操作:

bash
# 手动触发打包
$ git gc
Counting objects: 100, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (80/80), done.
Writing objects: 100% (100/100), done.
Total 100 (delta 20), reused 100 (delta 20)

# 查看打包文件
$ ls .git/objects/pack/
pack-abc1234567890abcdef1234567890abcdef12345678.idx
pack-abc1234567890abcdef1234567890abcdef12345678.pack

# 查看打包文件内容
$ git verify-pack -v .git/objects/pack/*.idx
non delta: 20 objects
delta: 80 objects

# 查看对象统计
$ git count-objects -v
count: 0
size: 0
in-pack: 100
packs: 1
size-pack: 50
prune-packable: 0
garbage: 0

1.4 常见错误与踩坑点

错误 1:不理解 Git 的快照机制

错误表现:

bash
# 误以为 Git 存储文件差异
$ git diff HEAD~1
# 认为这是 Git 存储的内容

产生原因:

  • 不理解 Git 存储的是快照而非差异
  • diff 命令是实时计算的,不是存储的内容

解决方案:

理解 Git 的快照存储:

bash
# Git 存储的是完整快照
$ git ls-tree -r HEAD
100644 blob 8d01495a...    README.md
100644 blob e69de29b...    config.php

# 每个提交都包含完整的文件快照
$ git ls-tree -r HEAD~1
100644 blob 8d01495a...    README.md
100644 blob abc12345...    config.php  # 不同的 blob

# diff 是实时计算的
$ git diff HEAD~1 HEAD
# Git 比较两个快照,计算差异显示

错误 2:不理解 Git 的对象复用

错误表现:

bash
# 担心修改一个文件会占用大量空间
$ git add large-file.zip
$ git commit -m "添加大文件"
# 修改文件
$ git add large-file.zip
$ git commit -m "修改大文件"
# 担心存储了两份

产生原因:

  • 不理解 Git 通过 SHA-1 哈希复用对象
  • 相同内容的文件只存储一次

解决方案:

bash
# 相同内容的文件共享同一个 blob
$ echo "Hello" > a.txt
$ echo "Hello" > b.txt
$ git add a.txt b.txt
$ git commit -m "添加两个相同内容的文件"

# 查看对象
$ git ls-tree -r HEAD
100644 blob e965047...    a.txt
100644 blob e965047...    b.txt  # 相同的哈希

# 只存储了一个 blob
$ git cat-file -p e965047
Hello

错误 3:不理解 Git 的垃圾回收

错误表现:

bash
# 删除分支后,提交仍然存在
$ git branch -D feature
$ git log --all
# 看不到 feature 分支的提交

# 但提交对象还在
$ git fsck --unreachable
unreachable commit abc1234...

产生原因:

  • 不理解 Git 的垃圾回收机制
  • 删除分支只是删除引用,对象仍然存在

解决方案:

bash
# 理解 Git 的垃圾回收时机
# 1. 默认 2 周后自动清理
# 2. 手动触发 git gc

# 查看不可达对象
$ git fsck --unreachable
unreachable commit abc1234...

# 恢复不可达对象
$ git branch recovered abc1234

# 手动触发垃圾回收
$ git gc --prune=now
# 立即清理不可达对象

# 查看垃圾回收配置
$ git config --get gc.pruneExpire
2.weeks.ago

错误 4:不理解 Git 的文件模式

错误表现:

bash
# 在 Windows 上修改文件权限后,Git 显示文件被修改
$ git status
modified:   script.sh

$ git diff script.sh
# 显示没有内容差异

产生原因:

  • 不理解 Git 记录文件模式
  • Windows 文件系统不支持 Unix 权限

解决方案:

bash
# 查看文件模式
$ git ls-files --stage
100644 8d01495a...    README.md    # 普通文件
100755 e69de29b...    script.sh    # 可执行文件

# 修改文件模式
$ git update-index --chmod=+x script.sh

# 或使用
$ git add --chmod=+x script.sh

# 配置 Git 忽略文件模式
$ git config core.fileMode false

错误 5:不理解 Git 的符号链接

错误表现:

bash
# 创建符号链接后,Git 跟踪了目标文件
$ ln -s ../config config
$ git add config
$ git status
# 显示为普通文件

产生原因:

  • 不理解 Git 对符号链接的处理
  • 没有正确识别符号链接模式

解决方案:

bash
# Git 正确识别符号链接
$ ls -la config
lrwxr-xr-x  1 user  staff  8 Jan 15 10:00 config -> ../config

$ git ls-files --stage
120000 e69de29b...    config    # 120000 表示符号链接

# 符号链接存储的是目标路径
$ git cat-file -p e69de29b
../config

# 注意:不要跟踪符号链接指向的文件
# 应该让 Git 跟踪符号链接本身

1.5 常见应用场景

场景 1:手动创建 Git 提交

场景描述: 使用底层命令手动创建一个完整的 Git 提交,深入理解 Git 的工作流程。

使用方法:

  1. 创建 blob 对象
  2. 创建树对象
  3. 创建提交对象
  4. 更新分支引用

示例代码:

bash
# 创建工作目录
$ mkdir manual-git
$ cd manual-git
$ git init

# 创建文件
$ echo "Hello, Git" > README.md
$ mkdir src
$ echo "<?php echo 'Hello';" > src/main.php

# 步骤 1:创建 blob 对象
$ git hash-object -w README.md
8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a

$ git hash-object -w src/main.php
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

# 步骤 2:创建树对象
# 先创建 src 目录的树
$ git update-index --add --cacheinfo 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 main.php
$ git write-tree
4b825dc6dcb235d07a23c6a48a8a8a6a6a6a6a6a

# 清空索引
$ git read-tree --empty

# 创建根目录的树
$ git update-index --add --cacheinfo 100644 8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a README.md
$ git update-index --add --cacheinfo 040000 4b825dc6dcb235d07a23c6a48a8a8a6a6a6a6a6a src
$ git write-tree
abc1234567890abcdef1234567890abcdef12345678

# 步骤 3:创建提交对象
$ echo "feat: 初始化项目" | git commit-tree abc1234567890abcdef1234567890abcdef12345678
def5678901234567890abcdef1234567890abcdef

# 步骤 4:更新分支引用
$ git update-ref refs/heads/main def5678901234567890abcdef1234567890abcdef

# 验证结果
$ git log --oneline
def5678 feat: 初始化项目

$ git ls-tree -r HEAD
100644 blob 8d01495a...    README.md
100644 blob e69de29b...    src/main.php

运行结果分析:

  • 理解了 Git 提交的完整过程
  • 每个步骤都可以单独操作
  • 高层命令封装了这些底层操作

场景 2:恢复丢失的提交

场景描述: 使用 Git 的底层机制恢复意外丢失的提交。

使用方法:

  1. 使用 reflog 查找丢失的提交
  2. 使用 fsck 查找不可达对象
  3. 恢复提交

示例代码:

bash
# 创建测试提交
$ git init
$ echo "Version 1" > file.txt
$ git add .
$ git commit -m "Version 1"
[main (root-commit) abc1234] Version 1

$ echo "Version 2" > file.txt
$ git add .
$ git commit -m "Version 2"
[main def5678] Version 2

$ echo "Version 3" > file.txt
$ git add .
$ git commit -m "Version 3"
[main ghi9012] Version 3

# 模拟误操作:重置到第一个提交
$ git reset --hard abc1234
HEAD is now at abc1234 Version 1

# 查看当前状态
$ git log --oneline
abc1234 Version 1

# 方法 1:使用 reflog 恢复
$ git reflog
abc1234 HEAD@{0}: reset: moving to abc1234
ghi9012 HEAD@{1}: commit: Version 3
def5678 HEAD@{2}: commit: Version 2
abc1234 HEAD@{3}: commit (initial): Version 1

# 恢复到 Version 3
$ git reset --hard ghi9012
HEAD is now at ghi9012 Version 3

# 方法 2:使用 fsck 查找不可达对象
$ git reset --hard abc1234
$ git fsck --unreachable
unreachable commit ghi9012...
unreachable commit def5678...

# 恢复特定提交
$ git branch recovered ghi9012
$ git checkout recovered
Switched to branch 'recovered'

# 方法 3:直接查看对象
$ git cat-file -p ghi9012
tree 4b825dc6...
parent def5678...
author Zhang San <zhang@example.com> 1705287600 +0800

Version 3

运行结果分析:

  • Git 的对象一旦创建就不会立即删除
  • reflog 记录了所有 HEAD 的变更
  • fsck 可以找到所有不可达对象

场景 3:分析 Git 仓库性能

场景描述: 分析 Git 仓库的性能问题并进行优化。

使用方法:

  1. 查看仓库统计信息
  2. 分析大文件
  3. 执行优化操作

示例代码:

bash
# 查看仓库统计
$ git count-objects -v
count: 1000        # 松散对象数量
size: 5000         # 松散对象大小(KB)
in-pack: 5000      # 打包对象数量
packs: 5           # 打包文件数量
size-pack: 10000   # 打包文件大小(KB)
prune-packable: 0
garbage: 0

# 查看仓库大小
$ du -sh .git
50M     .git

# 查找大文件
$ git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  awk '/^blob/ {print substr($0,6)}' | \
  sort -n -k2 | \
  tail -20

# 查看打包文件详情
$ git verify-pack -v .git/objects/pack/*.idx | \
  sort -k 3 -n | \
  tail -20

# 执行优化
$ git gc --aggressive --prune=now
Counting objects: 6000, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5000/5000), done.
Writing objects: 100% (6000/6000), done.
Total 6000 (delta 3000), reused 6000 (delta 3000)

# 再次查看统计
$ git count-objects -v
count: 0
size: 0
in-pack: 6000
packs: 1
size-pack: 8000
prune-packable: 0
garbage: 0

运行结果分析:

  • git gc 整合打包文件,优化存储
  • --aggressive 执行更彻底的优化
  • 定期执行 gc 可以保持仓库性能

场景 4:理解 Git 的差异算法

场景描述: 深入理解 Git 如何计算和存储文件差异。

使用方法:

  1. 创建相似文件
  2. 查看差异计算
  3. 分析打包效果

示例代码:

bash
# 创建测试文件
$ git init
$ for i in {1..1000}; do echo "Line $i"; done > file.txt
$ git add file.txt
$ git commit -m "添加原始文件"

# 修改少量内容
$ sed -i '' 's/Line 500/Modified Line 500/' file.txt
$ git add file.txt
$ git commit -m "修改一行"

# 查看差异
$ git diff HEAD~1
diff --git a/file.txt b/file.txt
index abc1234..def5678 100644
--- a/file.txt
+++ b/file.txt
@@ -497,3 +497,3 @@
 Line 499
-Line 500
+Modified Line 500
 Line 501

# 查看存储的对象
$ git ls-tree -r HEAD
100644 blob def5678...    file.txt

$ git ls-tree -r HEAD~1
100644 blob abc1234...    file.txt

# 打包并查看差异存储
$ git gc
$ git verify-pack -v .git/objects/pack/*.idx
# 可以看到两个 blob 的关系
# def5678 可能存储为 abc1234 + delta

# 查看差异链
$ git cat-file -s def5678    # 对象大小
$ git cat-file -s abc1234    # 基础对象大小

运行结果分析:

  • Git 使用差异压缩优化存储
  • 相似文件只存储差异部分
  • 打包时自动计算最优差异链

场景 5:自定义 Git 命令

场景描述: 利用 Git 的底层原理创建自定义命令。

使用方法:

  1. 创建脚本
  2. 添加到 PATH
  3. 作为 Git 命令使用

示例代码:

bash
# 创建自定义命令:git-undo
$ cat > /usr/local/bin/git-undo << 'EOF'
#!/bin/bash
# 撤销最后一次提交,保留修改

if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then
    echo "错误:不在 Git 仓库中"
    exit 1
fi

if [ "$(git rev-parse HEAD 2>/dev/null)" = "" ]; then
    echo "错误:没有任何提交"
    exit 1
fi

echo "将要撤销的提交:"
git log -1 --oneline
echo ""
read -p "确认撤销?(y/n) " -n 1 -r
echo ""

if [[ $REPLY =~ ^[Yy]$ ]]; then
    git reset --soft HEAD~1
    echo "已撤销提交,修改保留在暂存区"
fi
EOF

$ chmod +x /usr/local/bin/git-undo

# 使用自定义命令
$ git undo
将要撤销的提交:
abc1234 feat: 最新提交

确认撤销?(y/n) y
已撤销提交,修改保留在暂存区

# 创建自定义命令:git-stats
$ cat > /usr/local/bin/git-stats << 'EOF'
#!/bin/bash
# 显示仓库统计信息

echo "=== Git 仓库统计 ==="
echo ""
echo "提交数量:$(git rev-list --count HEAD)"
echo "分支数量:$(git branch -a | wc -l | tr -d ' ')"
echo "标签数量:$(git tag | wc -l | tr -d ' ')"
echo "贡献者数量:$(git shortlog -sn | wc -l | tr -d ' ')"
echo ""
echo "=== 对象统计 ==="
git count-objects -v
echo ""
echo "=== 仓库大小 ==="
du -sh .git
echo ""
echo "=== 最近 5 次提交 ==="
git log --oneline -5
EOF

$ chmod +x /usr/local/bin/git-stats

# 使用
$ git stats
=== Git 仓库统计 ===

提交数量:100
分支数量:5
标签数量:3
贡献者数量:2

=== 对象统计 ===
count: 0
size: 0
in-pack: 500
packs: 1
size-pack: 100
prune-packable: 0
garbage: 0

=== 仓库大小 ===
10M     .git

=== 最近 5 次提交 ===
abc1234 feat: 最新提交
...

运行结果分析:

  • Git 会自动识别 git-xxx 格式的命令
  • 可以封装常用的复杂操作
  • 提高工作效率

1.6 企业级进阶应用

Git 仓库维护

定期维护任务:

bash
# 日常维护脚本
#!/bin/bash
# git-maintenance.sh

echo "开始 Git 仓库维护..."

# 1. 清理不可达对象
echo "清理不可达对象..."
git prune --expire=now

# 2. 打包优化
echo "打包优化..."
git gc --aggressive

# 3. 清理远程分支引用
echo "清理远程分支引用..."
git remote prune origin

# 4. 重新打包索引
echo "重新打包索引..."
git repack -a -d -f --depth=250 --window=250

# 5. 验证仓库完整性
echo "验证仓库完整性..."
git fsck --full

echo "维护完成!"

# 显示统计
git count-objects -v

自动化维护:

bash
# 配置自动维护
$ git config --global maintenance.auto true

# 配置维护策略
$ git config --global maintenance.strategy incremental

# 设置定时任务(Cron)
$ crontab -e
# 每周日凌晨 2 点执行维护
0 2 * * 0 cd /path/to/repo && git gc --aggressive --prune=now

Git 性能优化

大仓库优化:

bash
# 1. 部分克隆(只克隆最近的历史)
$ git clone --depth 1 https://github.com/large/repo.git

# 2. 单分支克隆
$ git clone --single-branch --branch main https://github.com/large/repo.git

# 3. 稀疏检出
$ git clone --filter=blob:none --sparse https://github.com/large/repo.git
$ cd repo
$ git sparse-checkout init --cone
$ git sparse-checkout set src/app

# 4. 配置性能选项
$ git config core.packedGitLimit 512m
$ git config core.packedGitWindowSize 32k
$ git config pack.deltaCacheSize 2047m
$ git config pack.packSizeLimit 2047m
$ git config pack.windowMemory 2047m

# 5. 使用文件系统监控
$ git config core.fsmonitor true
$ git config core.untrackedCache true

Git 服务器优化

服务器端配置:

bash
# 服务器端 Git 配置
$ git config --system receive.denyNonFastForwards true
$ git config --system receive.denyDeletes true
$ git config --system core.sharedRepository group

# 启用自动打包
$ git config --global repack.writeBitmaps true

# 设置钩子
$ cat > /path/to/repo.git/hooks/post-receive << 'EOF'
#!/bin/bash
# 接收推送后自动打包
git gc --auto
EOF

$ chmod +x /path/to/repo.git/hooks/post-receive

1.7 行业最佳实践

实践 1:定期执行垃圾回收

实践内容: 定期执行 git gc 保持仓库性能。

推荐理由:

  • 整合松散对象
  • 优化打包文件
  • 提高操作速度
bash
# 每周执行一次
$ git gc

# 深度优化(每月一次)
$ git gc --aggressive

# 立即清理
$ git gc --prune=now

实践 2:使用浅克隆

实践内容: 对于大型仓库,使用浅克隆减少下载量。

推荐理由:

  • 减少下载时间
  • 节省磁盘空间
  • CI/CD 环境特别适用
bash
# 只克隆最近一次提交
$ git clone --depth 1 https://github.com/large/repo.git

# 后续可以获取更多历史
$ git fetch --unshallow

实践 3:配置合理的忽略规则

实践内容: 配置完善的 .gitignore,避免跟踪不必要的文件。

推荐理由:

  • 减少仓库体积
  • 提高操作速度
  • 避免冲突
bash
# 使用模板
$ git ignore-io -t vim,php,macos > .gitignore

# 或使用在线生成器
# https://www.toptal.com/developers/gitignore

实践 4:使用 Git LFS 管理大文件

实践内容: 对于大型二进制文件,使用 Git LFS。

推荐理由:

  • 避免仓库膨胀
  • 提高克隆速度
  • 节省存储空间
bash
# 安装 Git LFS
$ brew install git-lfs

# 初始化
$ git lfs install

# 追踪大文件
$ git lfs track "*.psd"
$ git lfs track "*.mp4"

实践 5:定期备份重要分支

实践内容: 使用标签或备份分支保护重要的开发进度。

推荐理由:

  • 防止意外删除
  • 便于回溯
  • 保护工作成果
bash
# 为重要节点打标签
$ git tag -a backup-2024-01-15 -m "备份点"

# 创建备份分支
$ git branch backup/main-$(date +%Y%m%d) main

# 推送到远程备份
$ git push origin --tags
$ git push origin backup/main-20240115

1.8 常见问题答疑(FAQ)

问题 1:Git 如何保证数据完整性?

问题描述: 想知道 Git 如何确保存储的数据不被损坏。

回答内容:

Git 使用多种机制保证数据完整性:

1. SHA-1 哈希校验:

每个 Git 对象都有一个 SHA-1 哈希值,这个值是根据对象内容计算的:

bash
# 对象格式
<类型> <大小>\0<内容>

# 计算哈希
$ echo -n "blob 11\0Hello, Git" | shasum
8d01495a85a9f6e2f8e9bc4b5a5c5a5a5a5a5a5a

任何内容的改变都会导致哈希值变化,因此无法在不被发现的情况下修改对象。

2. 链式结构:

每个提交都包含指向树对象的引用,树对象包含指向 blob 的引用:

bash
Commit (SHA-1)
└── Tree (SHA-1)
    ├── Blob (SHA-1)
    └── Blob (SHA-1)

修改任何一级都会影响所有上级的哈希值。

3. 完整性验证:

bash
# 验证仓库完整性
$ git fsck --full
Checking objects: 100% (1000/1000), done.
Checking connectivity: 100% (100/100), done.

# 发现损坏的对象
$ git fsck
error: sha1 mismatch 8d01495a...

问题 2:为什么 Git 比 SVN 快?

问题描述: 想了解 Git 性能优势的技术原因。

回答内容:

Git 的性能优势来自多个方面:

1. 本地操作:

bash
# Git:本地操作,无需网络
$ git log          # 立即显示
$ git diff         # 立即显示
$ git branch       # 立即显示

# SVN:需要访问服务器
$ svn log          # 需要网络请求
$ svn diff         # 需要网络请求

2. 快照存储:

Git 存储完整快照,而不是文件差异:

bash
# Git:直接访问快照
$ git checkout HEAD~10  # 直接切换到快照

# SVN:需要应用差异
$ svn update -r 10      # 需要计算和应用差异

3. 高效的打包:

bash
# Git 自动打包和压缩
$ git gc
# 使用 delta 压缩,但访问时不需要解压

4. 索引优化:

bash
# Git 使用索引快速判断文件状态
$ git status  # 通过比较索引和工作目录

问题 3:Git 如何处理大文件?

问题描述: 仓库中有大型文件,如何优化存储和性能。

回答内容:

1. 使用 Git LFS:

bash
# 安装 Git LFS
$ brew install git-lfs
$ git lfs install

# 追踪大文件类型
$ git lfs track "*.zip"
$ git lfs track "*.mp4"
$ git lfs track "*.psd"

# 查看追踪规则
$ git lfs track
Listing tracked patterns
    *.zip (.gitattributes)
    *.mp4 (.gitattributes)
    *.psd (.gitattributes)

# 正常提交
$ git add .
$ git commit -m "添加大文件"
$ git push

2. 使用 .gitignore:

bash
# 忽略不需要版本控制的大文件
$ cat >> .gitignore << 'EOF'
*.zip
*.tar.gz
*.mp4
*.mov
node_modules/
vendor/
EOF

3. 清理历史中的大文件:

bash
# 使用 git filter-branch 删除历史中的大文件
$ git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch path/to/large-file.zip' \
  --prune-empty --tag-name-filter cat -- --all

# 或使用 BFG Repo-Cleaner(更快)
$ bfg --delete-files large-file.zip
$ git reflog expire --expire=now --all
$ git gc --prune=now --aggressive

问题 4:Git 如何实现分支的快速创建?

问题描述: 为什么 Git 创建分支这么快?

回答内容:

Git 分支创建快的原因:

1. 分支只是 41 字节的文件:

bash
# 创建分支
$ git branch feature

# 实际操作:创建一个 41 字节的文件
$ cat .git/refs/heads/feature
abc1234567890abcdef1234567890abcdef12345678

# 文件大小
$ wc -c .git/refs/heads/feature
41 .git/refs/heads/feature

2. 不复制任何代码:

bash
# SVN 创建分支:复制整个目录
$ svn copy trunk branches/feature
# 需要复制所有文件

# Git 创建分支:只创建引用
$ git branch feature
# 只创建一个指针

3. 共享提交历史:

bash
# 所有分支共享同一个对象数据库
$ git branch feature
$ git branch hotfix

# 两个分支指向同一个提交
$ cat .git/refs/heads/feature
abc1234...

$ cat .git/refs/heads/hotfix
abc1234...  # 相同的哈希

问题 5:Git 的 reflog 是什么?

问题描述: 听说 reflog 可以恢复丢失的提交,想了解其原理。

回答内容:

reflog 原理:

reflog 记录了 HEAD 和分支引用的所有变更历史:

bash
# 查看 HEAD 的 reflog
$ git reflog
ghi9012 HEAD@{0}: commit: Version 3
def5678 HEAD@{1}: commit: Version 2
abc1234 HEAD@{2}: checkout: moving from feature to main
xyz5678 HEAD@{3}: commit: Feature commit

# 查看特定分支的 reflog
$ git reflog show main
ghi9012 main@{0}: commit: Version 3
def5678 main@{1}: commit: Version 2
abc1234 main@{2}: branch: Created from abc1234

# reflog 存储位置
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 abc1234567890abcdef... Zhang San <zhang@example.com> 1705287600 +0800    commit (initial): Initial commit

使用 reflog 恢复:

bash
# 误删分支
$ git branch -D feature

# 使用 reflog 找到分支的最后一个提交
$ git reflog
abc1234 HEAD@{5}: checkout: moving from main to feature

# 恢复分支
$ git branch feature abc1234

# 误执行 reset
$ git reset --hard HEAD~5

# 使用 reflog 恢复
$ git reflog
ghi9012 HEAD@{1}: reset: moving to HEAD~5
abc1234 HEAD@{2}: commit: Important commit

$ git reset --hard abc1234

reflog 过期:

bash
# reflog 默认保留 90 天
$ git config --get gc.reflogExpire
90.days.ago

# 不可达对象默认保留 30 天
$ git config --get gc.reflogExpireUnreachable
30.days.ago

# 立即清理
$ git reflog expire --expire=now --all
$ git gc --prune=now

问题 6:Git 如何处理文件名编码?

问题描述: 在不同操作系统间协作时,文件名编码出现问题。

回答内容:

Git 的文件名编码处理:

bash
# Git 使用 UTF-8 编码存储文件名
$ git config core.quotePath
true

# 关闭路径引用(显示非 ASCII 字符)
$ git config core.quotePath false

# 查看文件名编码问题
$ git ls-files
"test/\344\270\255\346\226\207.txt"  # 转义的中文

# 关闭引用后
$ git ls-files
test/中文.txt  # 正常显示

跨平台协作:

bash
# 配置忽略大小写(Windows/macOS 不区分大小写)
$ git config core.ignorecase true

# 但推荐保持大小写一致
$ git mv oldname.txt NewName.txt

# 处理换行符
$ git config core.autocrlf
# Windows: true
# Linux/macOS: input

# 配置 .gitattributes
$ cat > .gitattributes << 'EOF'
* text=auto
*.php text eol=lf
*.sh text eol=lf
*.bat text eol=crlf
EOF

1.9 实战练习

基础练习:手动创建 Git 对象

练习目标: 使用底层命令创建 blob、tree 和 commit 对象。

解题思路:

  1. 创建 blob 对象
  2. 创建树对象
  3. 创建提交对象

常见误区:

  • 忘记使用 -w 参数存储对象
  • 树对象需要先创建子目录的树

分步提示:

bash
# 步骤 1:创建仓库
$ mkdir git-objects
$ cd git-objects
$ git init

# 步骤 2:创建 blob 对象
# 提示:使用 git hash-object -w

# 步骤 3:创建树对象
# 提示:使用 git update-index 和 git write-tree

# 步骤 4:创建提交对象
# 提示:使用 git commit-tree

# 步骤 5:验证结果
# 提示:使用 git cat-file 查看对象

参考代码:

bash
# 创建仓库
$ mkdir git-objects
$ cd git-objects
$ git init

# 创建 blob 对象
$ echo "Hello, World" | git hash-object -w --stdin
557db03de997c86a4a028e1ebd3a1ceb225be238

# 验证
$ git cat-file -t 557db03
blob
$ git cat-file -p 557db03
Hello, World

# 创建树对象
$ git update-index --add --cacheinfo 100644 557db03de997c86a4a028e1ebd3a1ceb225be238 hello.txt
$ git write-tree
0194f5260c91f0f0e2f0e0e0e0e0e0e0e0e0e0e0

# 验证树对象
$ git cat-file -t 0194f52
tree
$ git cat-file -p 0194f52
100644 blob 557db03de997c86a4a028e1ebd3a1ceb225be238    hello.txt

# 创建提交对象
$ echo "Initial commit" | git commit-tree 0194f52
abc1234567890abcdef1234567890abcdef12345678

# 验证提交对象
$ git cat-file -t abc1234
commit
$ git cat-file -p abc1234
tree 0194f5260c91f0f0e2f0e0e0e0e0e0e0e0e0e0e0
author Zhang San <zhang@example.com> 1705287600 +0800
committer Zhang San <zhang@example.com> 1705287600 +0800

Initial commit

# 更新分支引用
$ git update-ref refs/heads/main abc1234

# 验证
$ git log --oneline
abc1234 Initial commit

进阶练习:分析 Git 仓库结构

练习目标: 深入分析 Git 仓库的内部结构。

解题思路:

  1. 查看仓库目录结构
  2. 分析对象数据库
  3. 理解引用关系

常见误区:

  • 不理解打包文件的作用
  • 忽略 reflog 的存在

分步提示:

bash
# 步骤 1:创建测试仓库
$ mkdir analyze-repo
$ cd analyze-repo
$ git init
$ echo "test" > file.txt
$ git add .
$ git commit -m "Initial"

# 步骤 2:查看 .git 目录结构
# 提示:使用 tree 或 ls -la

# 步骤 3:分析对象
# 提示:查看 objects 目录

# 步骤 4:分析引用
# 提示:查看 refs 目录

# 步骤 5:分析索引
# 提示:查看 index 文件

参考代码:

bash
# 创建测试仓库
$ mkdir analyze-repo
$ cd analyze-repo
$ git init
$ echo "test content" > file.txt
$ git add .
$ git commit -m "Initial commit"
[main (root-commit) abc1234] Initial commit
 1 file changed, 1 insertion(+)

# 查看 .git 目录
$ tree .git -L 2
.git
├── HEAD
├── config
├── description
├── hooks/
├── info/
   └── exclude
├── objects/
   ├── info/
   └── pack/
└── refs/
    ├── heads/
   └── main
    └── tags/

# 查看 HEAD
$ cat .git/HEAD
ref: refs/heads/main

# 查看分支引用
$ cat .git/refs/heads/main
abc1234567890abcdef1234567890abcdef12345678

# 查看对象
$ find .git/objects -type f
.git/objects/ab/c1234567890abcdef1234567890abcdef12345678
.git/objects/4b/825dc6dcb235d07a23c6a48a8a8a6a6a6a6a6a6a
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391

# 分析提交对象
$ git cat-file -p abc1234
tree 4b825dc6dcb235d07a23c6a48a8a8a6a6a6a6a6a6a
author Zhang San <zhang@example.com> 1705287600 +0800
committer Zhang San <zhang@example.com> 1705287600 +0800

Initial commit

# 分析树对象
$ git cat-file -p 4b825dc6
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    file.txt

# 分析 blob 对象
$ git cat-file -p e69de29b
test content

# 查看索引
$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 file.txt

# 查看 reflog
$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 abc1234567890abcdef... Zhang San <zhang@example.com> 1705287600 +0800    commit (initial): Initial commit

挑战练习:实现 Git 仓库清理脚本

练习目标: 编写一个脚本,清理 Git 仓库中的无用对象并优化性能。

解题思路:

  1. 分析仓库状态
  2. 清理不可达对象
  3. 打包优化
  4. 验证完整性

常见误区:

  • 清理前没有备份
  • 忘记验证完整性

分步提示:

bash
# 步骤 1:创建清理脚本
# 提示:包含统计、清理、优化、验证功能

# 步骤 2:添加安全检查
# 提示:检查是否有未提交的修改

# 步骤 3:执行清理
# 提示:使用 git gc 和 git prune

# 步骤 4:验证结果
# 提示:使用 git fsck 验证

参考代码:

bash
#!/bin/bash
# git-cleanup.sh - Git 仓库清理脚本

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 检查是否在 Git 仓库中
if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then
    echo -e "${RED}错误:当前目录不是 Git 仓库${NC}"
    exit 1
fi

echo -e "${GREEN}=== Git 仓库清理工具 ===${NC}"
echo ""

# 显示当前状态
echo -e "${YELLOW}当前仓库状态:${NC}"
git count-objects -v
echo ""

# 检查工作目录状态
if [ -n "$(git status --porcelain)" ]; then
    echo -e "${YELLOW}警告:工作目录有未提交的修改${NC}"
    git status --short
    echo ""
    read -p "是否继续清理?(y/n) " -n 1 -r
    echo ""
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

# 清理远程分支引用
echo -e "${YELLOW}清理远程分支引用...${NC}"
git remote prune origin 2>/dev/null || true

# 清理 reflog
echo -e "${YELLOW}清理 reflog...${NC}"
git reflog expire --expire=now --all

# 清理不可达对象
echo -e "${YELLOW}清理不可达对象...${NC}"
git prune --expire=now

# 打包优化
echo -e "${YELLOW}执行打包优化...${NC}"
git gc --aggressive --prune=now

# 验证完整性
echo -e "${YELLOW}验证仓库完整性...${NC}"
if git fsck --full; then
    echo -e "${GREEN}仓库完整性验证通过${NC}"
else
    echo -e "${RED}警告:仓库完整性验证失败${NC}"
fi

echo ""
echo -e "${GREEN}清理完成!${NC}"
echo ""
echo -e "${YELLOW}清理后状态:${NC}"
git count-objects -v
echo ""
echo -e "${YELLOW}仓库大小:${NC}"
du -sh .git

# 使用方法
# $ chmod +x git-cleanup.sh
# $ ./git-cleanup.sh

1.10 知识点总结

核心要点

  1. Git 是内容寻址文件系统

    • 使用 SHA-1 哈希标识对象
    • 对象存储在 .git/objects 目录
    • 相同内容只存储一次
  2. Git 存储快照而非差异

    • 每次提交保存完整快照
    • 打包时使用差异压缩
    • 访问时不需要解压
  3. 分支是轻量级引用

    • 分支只是 41 字节的文件
    • 创建和切换非常快速
    • 所有分支共享对象库
  4. 索引是工作目录和仓库的桥梁

    • 记录文件的元数据
    • 支持部分暂存
    • 加速状态检查
  5. Git 有完善的完整性保障

    • SHA-1 哈希校验
    • 链式结构
    • fsck 验证工具

易错点回顾

易错点正确做法
误解 Git 存储机制理解快照存储和差异压缩
忽略对象复用相同内容共享 blob
不理解垃圾回收定期执行 git gc
文件模式问题配置 core.fileMode
符号链接处理理解 120000 模式

1.11 拓展参考资料

官方文档

进阶学习路径

  1. Git 底层命令

    • hash-object
    • cat-file
    • update-index
    • write-tree
    • commit-tree
  2. Git 协议

    • 传输协议
    • 智能协议
    • SSH/HTTPS
  3. Git 扩展开发

    • libgit2
    • git2go
    • pygit2

推荐资源


学习建议:

  • 使用底层命令手动创建提交,理解 Git 内部机制
  • 分析真实仓库的对象结构
  • 阅读 Git 源代码,深入理解实现

知识点承接:什么是 GitGit 核心概念 → 本教程