Git 与 GitHub|第一阶段:仓库、版本历史与 Git 对象模型(上篇)

从 Git 与 GitHub 的基础概念,到仓库、文件、版本与 Git 对象模型

文章目录

  • [一、GitHub 与 Git 为什么存在?它们分别解决什么问题?](#一、GitHub 与 Git 为什么存在?它们分别解决什么问题?)
    • [1. 软件项目为什么需要版本管理?](#1. 软件项目为什么需要版本管理?)
    • [2. Git 是什么?](#2. Git 是什么?)
    • [3. GitHub 是什么?](#3. GitHub 是什么?)
    • [4. Git、本地电脑与 GitHub 服务器之间的关系](#4. Git、本地电脑与 GitHub 服务器之间的关系)
  • 二、Repository:仓库究竟是什么?
    • [1. Repository 的含义](#1. Repository 的含义)
    • [2. 一个 GitHub 账号可以拥有多个仓库](#2. 一个 GitHub 账号可以拥有多个仓库)
    • [3. Public 与 Private](#3. Public 与 Private)
    • [4. 仓库首页的主要区域](#4. 仓库首页的主要区域)
  • [三、GitHub 文件系统:文件、目录、路径与 Markdown](#三、GitHub 文件系统:文件、目录、路径与 Markdown)
    • [1. 仓库根目录与相对路径](#1. 仓库根目录与相对路径)
    • [2. Git 为什么不能正常独立跟踪空目录?](#2. Git 为什么不能正常独立跟踪空目录?)
    • [3. GitHub 能否直接操作目录?](#3. GitHub 能否直接操作目录?)
    • [4. README.md 和 Markdown](#4. README.md 和 Markdown)
  • [四、GitHub 网页端如何管理文件?](#四、GitHub 网页端如何管理文件?)
    • [1. 创建仓库与初始化 README](#1. 创建仓库与初始化 README)
    • [2. 新建文件](#2. 新建文件)
    • [3. 上传已有文件](#3. 上传已有文件)
    • [4. 重命名文件](#4. 重命名文件)
    • [5. 移动文件](#5. 移动文件)
    • [6. 删除文件](#6. 删除文件)
    • [7. 通过新提交恢复文件](#7. 通过新提交恢复文件)
  • 五、Commit:保存版本的真正机制
    • [1. 保存文件不等于提交](#1. 保存文件不等于提交)
    • [2. Commit 究竟是什么?](#2. Commit 究竟是什么?)
    • [3. Commit Hash 与 Commit Message 的区别](#3. Commit Hash 与 Commit Message 的区别)
    • [4. Commit 表示完整文件版本,是否意味着复制所有文件?](#4. Commit 表示完整文件版本,是否意味着复制所有文件?)
  • [六、Blob、Tree 与 Commit 的对象引用结构](#六、Blob、Tree 与 Commit 的对象引用结构)
    • [1. Blob Object:保存内容](#1. Blob Object:保存内容)
    • [2. Tree Object:组织目录](#2. Tree Object:组织目录)
    • [3. 父提交引用与文件树引用是两条不同的关系](#3. 父提交引用与文件树引用是两条不同的关系)
    • [4. Blob 复用的条件](#4. Blob 复用的条件)
    • [5. Tree 复用的条件](#5. Tree 复用的条件)
    • [6. 两个不同 Commit 能否共享相同根 Tree?](#6. 两个不同 Commit 能否共享相同根 Tree?)
    • [7. Git 对象与磁盘物理存储的区别](#7. Git 对象与磁盘物理存储的区别)
  • [七、Git 版本历史和文件差异如何查看?](#七、Git 版本历史和文件差异如何查看?)
    • [1. Commit History:提交历史](#1. Commit History:提交历史)
    • [2. Diff 是什么?](#2. Diff 是什么?)
    • [3. 仓库提交历史与单个文件历史的区别](#3. 仓库提交历史与单个文件历史的区别)
    • [4. 文件路径历史与完整提交历史可能具有不同显示结果](#4. 文件路径历史与完整提交历史可能具有不同显示结果)
  • 八、第一部分知识关系

一、GitHub 与 Git 为什么存在?它们分别解决什么问题?

1. 软件项目为什么需要版本管理?

假设正在开发一个 Java 商城项目:

复制代码
ShoppingSystem/
├── src/
│   ├── User.java
│   ├── Product.java
│   └── Order.java
├── pom.xml
└── README.md

第一天完成用户注册功能,第二天增加登录功能,第三天修改订单功能。

随着开发继续,可能遇到以下情况:

  • 修改新功能时引入错误,需要恢复修改前的代码。
  • 想知道某个文件是什么时候、因为什么原因改变的。
  • 希望比较不同版本究竟增加、删除或修改了哪些内容。
  • 两个人同时开发不同功能,需要将修改整合到同一个项目。
  • 需要将项目保存到远程,以便在不同设备之间访问和协作。

一种直接的版本保存方法是复制项目文件夹:

复制代码
ShoppingSystem-v1/
ShoppingSystem-v2/
ShoppingSystem-v3/
ShoppingSystem-final/
ShoppingSystem-final2/

这样虽然能够保留一些历史状态,但文件夹越来越多,版本之间的变化需要人工比较,而且多人协作时容易出现混乱。

由此产生了两个相互关联的需求:

版本控制 :记录项目历史,比较、恢复、合并不同版本。

远程托管与协作:让项目能够通过网络被访问,并提供多人协作、代码审查和自动化等服务。

Git 和 GitHub 分别围绕这些需求提供能力。

2. Git 是什么?

Git 是一种分布式版本控制系统(Distributed Version Control System)。

其主要能力包括:

  • 对项目文件创建版本记录。
  • 记录 Commit 之间的历史关系。
  • 创建、切换和管理不同 Branch。
  • 查看不同版本的文件状态和差异。
  • 恢复文件、撤销修改以及整合不同开发线。
  • 在本地管理完整的版本历史,并与远程仓库交换数据。

Git 可以独立于 GitHub 工作。

例如,在 Windows 电脑上安装 Git 后,即使暂时没有互联网,也可以在已有的本地 Git 仓库中创建提交、查看历史、创建分支和比较不同版本。

Git 本身并不要求必须使用 GitHub。

3. GitHub 是什么?

GitHub 是以 Git 仓库为核心的远程代码托管和软件开发协作平台。

GitHub 可以提供远程仓库存储,同时通过网站提供:

功能 主要作用
Repository 托管项目文件和 Git 版本历史
Code 浏览文件、提交、分支和项目内容
Issues 管理问题、Bug、需求及任务
Pull Requests 提出、审查和合并修改
Actions 根据事件与配置自动执行工作流
Projects 组织项目任务和进度
Releases 发布项目版本及关联发布文件
Settings 管理仓库权限、配置及其他设置

GitHub 可以提供超出 Git 基础版本控制能力的功能。

例如,Pull Request 的讨论界面、Issue 跟踪、GitHub Actions 的运行管理,都是 GitHub 平台层面的功能。

但 GitHub 中用于管理提交、分支和版本历史的核心基础仍然来自 Git。

4. Git、本地电脑与 GitHub 服务器之间的关系

假设你已经将一个项目同时保存在电脑和 GitHub 上。

复制代码
本地 Windows 电脑
│
├── 项目实际文件
│   ├── Main.java
│   └── README.md
│
└── 本地 Git 仓库
    ├── Commit 历史
    ├── Branch 引用
    └── Git 对象数据库

            网络通信
         push / fetch / pull
                ↕
GitHub 远程服务器
│
└── 远程 Git 仓库
    ├── 版本历史
    ├── 分支引用
    └── 文件版本数据

本地仓库和远程仓库可以分别存在,并且各自拥有自己的状态。

在电脑上使用编辑器保存一个文件,并不会自动意味着 GitHub 远程仓库中的文件也已改变。

典型的本地协作流程包含:

  1. 修改本地项目文件。
  2. 通过 Git 将选定修改记录为 Commit。
  3. 通过 Push 将本地相应提交及对象数据传送到远程仓库,并请求更新远程分支。

反方向则可以通过 Fetch 或 Pull 等操作取得远程更新。

其中 Fetch 与 Pull 并不完全相同,后续本地 Git 阶段会正式讲解。

在 GitHub 网页端直接编辑并提交文件时,不需要先由自己的电脑执行 Push。 GitHub 会在远程仓库中完成对应的提交与引用更新。

因此,网页操作与本地 Git 操作虽然都能修改仓库版本,但它们使用的操作环境不同。

二、Repository:仓库究竟是什么?

1. Repository 的含义

Repository,通常简称 Repo,中文称为仓库。

一个 Git 仓库用于管理项目的版本数据、提交对象、分支引用和其他 Git 元数据。

在一个典型的本地非裸仓库中,可能看到:

复制代码
MyProject/
├── .git/
├── src/
│   ├── Main.java
│   └── User.java
├── pom.xml
└── README.md

这里:

  • src/ 等目录保存项目的实际文件。
  • pom.xml 等文件属于项目内容。
  • .git/ 保存 Git 内部用于版本管理的对象、引用、配置等信息。

普通文件夹和 Git 仓库的区别,在于后者具有 Git 的版本管理数据。

.git 也不应被简单理解成"所有历史版本的普通文件夹备份集合"。Git 使用自己的对象模型保存和关联版本内容。

2. 一个 GitHub 账号可以拥有多个仓库

例如:

复制代码
GitHub 账号
│
├── java-learning
├── shopping-system
├── my-notes
└── github-study-lab

这些仓库可以分别维护自己的文件、分支和提交历史。

典型仓库地址:

复制代码
https://github.com/用户名/仓库名

例如,本次学习使用的练习仓库为:

复制代码
Everett-Lorne/github-study-lab

仓库名用于在所属账号或组织下区分不同仓库。

3. Public 与 Private

创建仓库时,需要选择可见性。

Public:公开仓库。 通常允许其他用户公开查看仓库内容。

Private:私有仓库。 访问受到权限控制,普通未授权用户无法直接查看。

本次 github-study-lab 选择了 Private。

但 Private 不能替代敏感信息安全管理。密码、API Token、私钥等敏感内容不应随意提交。

因为即使后来删除文件,旧提交也可能仍保存这些内容。

4. 仓库首页的主要区域

本次练习中,GitHub 仓库首页出现了:

  • main:当前网页选中的分支。
  • 1 Branch:当时仓库的分支数量。
  • 0 Tags:当时尚无 Tag。
  • README.md:根目录中的项目说明文件。
  • Add file:创建或上传文件。
  • Code:提供仓库访问、克隆等相关入口。
  • 2 Commits、3 Commits:可进入相关提交历史。
  • Watch、Fork、Star:GitHub 的关注、派生与收藏等功能。

Watch、Fork、Star 属于 GitHub 提供的不同功能:

  • Watch 与接收仓库活动通知有关。
  • Star 可以用于收藏或标记感兴趣的项目。
  • Fork 可以在自己的账号或组织下创建仓库的派生副本,用于独立修改及后续协作。

Fork 与 Git 内部的普通 Branch 不是同一对象。Fork 的完整协作流程尚待后续教学。

三、GitHub 文件系统:文件、目录、路径与 Markdown

1. 仓库根目录与相对路径

本次练习仓库曾包含:

复制代码
github-study-lab/
├── README.md
├── notes/
│   └── 01-github-basics.md
└── examples/
    └── github-upload-example.txt

在这里,github-study-lab/ 代表项目根目录。

从根目录出发,可以使用相对路径定位文件:

复制代码
README.md

notes/01-github-basics.md

examples/github-upload-example.txt

路径既包含文件名称,也表达其目录位置。

例如:

复制代码
notes/README.md
docs/README.md

虽然文件名相同,但它们位于不同路径,是不同的文件条目。

2. Git 为什么不能正常独立跟踪空目录?

在 Windows 资源管理器中,可以直接创建一个空文件夹。

但 Git 的版本文件树通过 Tree 对象组织文件和子目录条目。

Git 正常记录的是包含条目的目录结构,不会通过普通提交独立保留一个完全空的目录。

例如,创建:

复制代码
notes/

如果目录中没有文件或其他受跟踪内容,它通常不会出现在 Git 提交的文件树里。

因此,GitHub 网页上可以通过创建完整路径:

复制代码
notes/git-basics.md

同时形成目录和文件。

有些项目使用 .gitkeep 文件来保留本来想作为空目录使用的位置。

.gitkeep 是一种社区命名约定,不是 Git 内置的特殊对象类型。

3. GitHub 能否直接操作目录?

GitHub 网页端可以提供目录级删除等功能,但许多编辑操作主要围绕文件展开。

例如,在网页中把:

复制代码
demo.txt

移动到:

复制代码
examples/demo.txt

本质上是在生成下一版本的新文件树状态。

不要求服务器必须模拟 Windows 中的"剪切文件---进入目录---粘贴文件"这一整套界面动作。

同样,如果删除了 examples 目录中最后一个文件,新提交中通常就不再出现这个目录。

Git 内部仍然使用 Tree Object 表示目录层级;不能因为网页中没有目录的铅笔编辑按钮,就推断 Git 不存在目录对象。

4. README.md 和 Markdown

README 是仓库中常见的项目说明文件。

.md 通常表示 Markdown 格式。

Markdown 使用纯文本符号表达文档结构,例如:

复制代码
# 一级标题

## 二级标题

**加粗文字**

- 第一项
- 第二项

[GitHub](https://github.com)

GitHub 可以把 Markdown 源文本渲染成标题、粗体、列表和链接。

因此,浏览文件时需要区分:

  • 源文件实际保存的 Markdown 字符。
  • Markdown 经过 GitHub 渲染后的预览效果。

切换源代码与预览视图,不会因此修改 Git 仓库或创建新提交。

Raw 可以用于查看文件原始内容。

四、GitHub 网页端如何管理文件?

这一部分对应本次实际学习过的上传、创建、编辑、重命名、移动、删除和恢复操作。

1. 创建仓库与初始化 README

本次练习仓库使用:

复制代码
Repository name:
github-study-lab

Visibility:
Private

Add README:
开启

创建后,GitHub 形成初始化提交。

README 是最初版本中的一个文件。

随后通过 GitHub 编辑页面修改 README,并输入提交说明:

复制代码
Update learning notes

形成第二次提交。

你提供的实际仓库截图曾显示:

复制代码
当前分支:main
文件:README.md
提交数量:2 Commits
最新说明:Update learning notes
短提交 ID:419648f

这组信息能够表明仓库已经有两个提交,且最新提交说明为 Update learning notes。

2. 新建文件

GitHub 网页端可以通过:

复制代码
Add file
  → Create new file

创建文件。

本次创建过:

复制代码
notes/01-github-basics.md

随后提交:

复制代码
Add GitHub basics notes

结果是:

复制代码
github-study-lab/
├── README.md
└── notes/
    └── 01-github-basics.md

提交数量从 2 变成 3。

实际截图中最新提交短 ID 为:

复制代码
c1b7dd6

要注意,创建文件和创建提交是两个需要区分的步骤:

  • 网页编辑器中准备了新文件内容。
  • 确认 Commit 后,才把这次文件变化保存成仓库中的一个新版本。

3. 上传已有文件

如果文件已经存在于 Windows 电脑中,可以使用:

复制代码
Add file
  → Upload files

例如,上传:

复制代码
upload-demo.txt

然后提交:

复制代码
Upload practice file

网页上传不会默认删除电脑上原来的文件。

它将指定文件内容上传到 GitHub,并通过提交纳入远程仓库的版本历史。

浏览器上传还受到文件大小和数量限制。本次教学中介绍过,GitHub 文档列出的常见限制包括浏览器单文件 25 MiB、一次批量上传最多 100 个文件;普通 Git 上传对较大文件另有限制,特别大的文件需要考虑 Git LFS 等机制。

这些是平台上传条件,并不是 Git Blob 模型自身规定每个对象只能有 25 MiB。

4. 重命名文件

例如:

复制代码
upload-demo.txt

改成:

复制代码
github-upload-example.txt

文件内容可以完全不变。

在版本文件树中,发生变化的是路径条目。

Git 的重命名识别通常来自比较两个版本时,根据路径和内容相似度推断变化关系。

Git 并不要求每个文件始终拥有一个永久不变的"文件身份编号",用来专门记录它所有的重命名历史。

因此,重命名并同时大量修改文件内容时,Git 的 Diff 不一定总能识别为重命名,也可能表现为删除旧文件与新增文件。

5. 移动文件

例如:

复制代码
github-upload-example.txt

移动到:

复制代码
examples/github-upload-example.txt

移动前后的文件内容可以相同,但文件路径不同。

从 Git 的对象模型观察:

  • 旧提交的文件树包含旧路径。
  • 新提交的文件树包含新路径。
  • 两个版本可以引用同一个文件内容 Blob。

因此,路径变化不必然意味着文件内容重新完整存储一份。

6. 删除文件

例如,从当前版本中删除:

复制代码
examples/github-upload-example.txt

删除提交完成后,该文件不再出现在当前分支最新提交的文件树中。

但是旧提交仍可能包含该文件。

删除当前版本的文件,与从所有历史中彻底清除文件内容是不同操作。

例如:

复制代码
Commit A:文件存在
Commit B:文件被删除

如果 Commit A 仍然可以访问,就可以通过 A 查看旧文件。

这一点对恢复误删文件有价值,也解释了为什么不能单纯通过新的删除提交来清除曾经泄露到 Git 历史中的敏感信息。

7. 通过新提交恢复文件

本次练习中使用过一种简单的网页恢复方法:

  1. 在仓库提交历史中找到删除之前的版本。
  2. 浏览那个版本对应的文件树。
  3. 找到被删除的文本文件并读取旧内容。
  4. 返回当前分支。
  5. 重新创建相同路径的文件,并写入旧内容。
  6. 创建一个新的恢复提交。

历史可以表示为:

复制代码
Commit A:文件存在
    ↓
Commit B:文件删除
    ↓
Commit C:文件重新存在

这时 C 是新的提交。

它不会自动删除 B,也不会原地修改 A。

旧版本、删除操作和恢复操作都可以保留在提交历史中。

这里介绍的是通过网页重新添加文件的简单恢复方式。Git 后续还有 restore、revert、reset 等机制,它们的效果及适用条件不同。

五、Commit:保存版本的真正机制

1. 保存文件不等于提交

假设在 VS Code 中编辑:

复制代码
System.out.println("Hello");

然后修改为:

复制代码
System.out.println("Hello GitHub");

按 Ctrl + S,通常只是让编辑器将内容保存到本地文件。

这并不自动产生 Git Commit。

一个普通本地 Git 提交流程还需要将选定的文件状态准备好,再执行提交。

因此:

复制代码
编辑内容
    ↓
保存文件
    ↓
准备 Git 提交内容
    ↓
创建 Commit

可以多次保存文件后再创建一次 Commit。

Git 也允许一次 Commit 记录多个文件的变化,不要求每修改一个文件就提交一次。

2. Commit 究竟是什么?

Commit 是 Git 的一种对象类型。

一次 Commit 对应一个已记录的项目版本状态,并包含相应的元数据及对象引用。

典型 Commit 可以包含:

  • 根 Tree Object 的 ID。
  • 一个或多个父提交的对象 ID。
  • 作者和提交者信息。
  • 时间等元数据。
  • Commit Message。

例如:

复制代码
Commit C
├── tree   → Root Tree T
├── parent → Commit B
├── author / committer
└── message

对于普通的非初始提交,通常只有一个父提交。

初始提交一般没有父提交。

Merge Commit 可以有两个或更多父提交。

这里最重要的是:Commit 保存的是定位项目版本与历史关系所需的信息;项目的实际文件内容由 Tree、Blob 等对象进一步组织。

3. Commit Hash 与 Commit Message 的区别

例如:

复制代码
419648f

它是 GitHub 曾显示的一次提交对象 ID 的短形式。

而:

复制代码
Update learning notes

是对应的提交说明。

它们的作用不同。

Commit Message 由提交者编写,可以重复,主要供人理解提交目的。

Commit 对象 ID 则由 Git 的对象标识机制确定,用于定位具体对象。

完整对象 ID 与 Git 使用的哈希算法有关;网页常显示足以在相应上下文中区分对象的缩写形式。

不能把 Commit Message 当成唯一的版本标识。

4. Commit 表示完整文件版本,是否意味着复制所有文件?

不意味着。

假设 C1 对应项目:

复制代码
A.txt → AAA
B.txt → BBB

随后只修改 A.txt:

复制代码
A.txt → AAA-new
B.txt → BBB

产生 C2。

从逻辑状态上,C1 和 C2 都对应包含两个文件的完整项目版本。

但在 Git 的对象数据库中,未改变的 B.txt 可以继续引用原来的 Blob。

因此:

复制代码
Commit C1
├── A.txt → Blob A1
└── B.txt → Blob B1

Commit C2
├── A.txt → Blob A2
└── B.txt → Blob B1

这里是省略 Tree 层级的简化表示。

它说明:完整版本快照的逻辑语义,不等于每次提交必须将全部文件的字节重新复制一遍。

这两种层次必须区分。

六、Blob、Tree 与 Commit 的对象引用结构

1. Blob Object:保存内容

Blob 是 Git 的对象类型之一,用于保存文件内容的字节数据。

例如:

复制代码
A.txt
内容:Hello Git

可以关联某个 Blob X:

复制代码
Blob X
内容:Hello Git

Blob X 本身不包含 A.txt 这个路径名称。

另一个文件:

复制代码
B.txt
内容:Hello Git

也可以引用相同的 Blob X。

因此,不同文件名、不同文件路径可以关联同一个内容对象。

2. Tree Object:组织目录

Tree 是 Git 用于组织文件树的对象类型。

一个 Tree 中可以包含:

  • 文件或子目录名称。
  • 文件模式信息。
  • 所关联的 Blob、子 Tree 等对象的 ID。

例如:

复制代码
Project/
├── README.md
└── notes/
    └── git.md

对象关系可以表示为:

复制代码
Commit C
    │
    └── Root Tree
          │
          ├── README.md → Blob R
          │
          └── notes → Tree N
                       │
                       └── git.md → Blob G

从 Commit C 开始,Git 可以沿根 Tree 和子 Tree 的引用找到相应路径的内容。

Tree 中记录路径条目,Blob 保存内容数据,两者负责不同的信息。

3. 父提交引用与文件树引用是两条不同的关系

一个 Commit 至少涉及两类重要关系:

复制代码
Commit C
│
├── parent → Commit B
│
└── tree → Root Tree C

第一条关系用于连接版本历史。

第二条关系用于定位当前提交对应的文件树状态。

因此,Git 可以分别回答:

问题 A:C 之前是哪一个提交?

通过 C 中的 parent 引用找到 B。

问题 B:C 这个版本有哪些文件、内容是什么?

通过 C 的 tree 引用进入根 Tree,再沿目录条目访问 Blob。

不能把这两类引用都理解成"提交引用其他提交",因为 Tree 不是 Commit。

4. Blob 复用的条件

如果文件内容字节完全相同,可以复用同一个 Blob 对象。

例如:

复制代码
Commit C1:B.txt → Blob B1
Commit C2:B.txt → Blob B1

即使 C1 和 C2 是不同 Commit,B.txt 的内容对象也可以相同。

如果另一个文件 C.txt 恰好内容也完全相同,它同样可以引用 Blob B1。

所以,Blob 与文件路径并非固定的一对一关系。

5. Tree 复用的条件

如果一个目录的条目内容没有变化,其中的名称、文件模式和关联对象 ID 等均相同,那么它可以复用原来的 Tree Object。

假设 C1 对应:

复制代码
Project/
├── A.txt            → Blob A1
├── B.txt            → Blob B1
├── docs/
│   └── help.md      → Blob Help
└── src/
    └── Main.java    → Blob Main1

只修改:

复制代码
src/Main.java

使它由 Main1 变为 Main2。

那么 C2 可以具有:

复制代码
Project/
├── A.txt            → Blob A1
├── B.txt            → Blob B1
├── docs/
│   └── help.md      → Blob Help
└── src/
    └── Main.java    → Blob Main2

比较对象:

对象 C1 C2
A.txt 内容 Blob A1 复用 Blob A1
B.txt 内容 Blob B1 复用 Blob B1
docs 子目录 Tree Docs 复用 Tree Docs
src/Main.java 内容 Blob Main1 新 Blob Main2
src 子目录 Tree Src1 新 Tree Src2
根目录 Root Tree T1 新 Root Tree T2

src/Main.java 内容改变,产生新的 Blob 引用。

因此 src 目录 Tree 中的条目改变,其对象 ID 也随之改变。

根 Tree 原来引用 Tree Src1,现在需要引用 Tree Src2,所以根 Tree 同样改变。

但是没有变化的 docs 子 Tree 可以继续复用。

这个过程可以理解为:

复制代码
Main.java 内容变化
       ↓
新 Blob
       ↓
src 目录 Tree 的引用变化
       ↓
新 src Tree
       ↓
根 Tree 中的 src 引用变化
       ↓
新根 Tree

对象引用变化沿路径向上影响祖先 Tree,但不要求所有无关目录一起重新生成。

6. 两个不同 Commit 能否共享相同根 Tree?

可以。

例如,创建一个不改变项目文件状态的空提交:

复制代码
git commit --allow-empty -m "Record checkpoint"

可以生成一个新的 Commit,尽管文件树内容没有变化。

于是:

复制代码
Commit C1 → Root Tree T1
Commit C2 → Root Tree T1

两个 Commit 可以引用相同根 Tree,但 Commit 对象不同。

因为 C2 的父提交关系、时间或提交说明等信息与 C1 不同。

因此需要区分三个对象是否相同的判断条件:

对象 主要由什么内容确定
Blob 内容字节
Tree 目录条目及其关联对象
Commit 根 Tree、父提交、元数据、提交说明等

Git 使用内容寻址机制标识这些对象。

Blob、Tree、Commit 都可以通过对象 ID 被访问,但它们表示的信息和职责不同。

7. Git 对象与磁盘物理存储的区别

前面的模型解释了 Git 对象之间的逻辑关系。

至于对象如何在磁盘上压缩、打包、增量存储,属于另一个实现层次。

例如 Packfile 可以涉及对象压缩和 Delta 表示。

因此,"两个提交引用相同的 Blob"是对象模型层面的认识;不能直接把它等同于磁盘上一定存在两个普通文件或者某种固定的存储布局。

当前理解 Commit---Tree---Blob 的引用关系,已经足以解释正常的版本状态、对象复用和历史文件访问。

七、Git 版本历史和文件差异如何查看?

1. Commit History:提交历史

假设仓库经历:

复制代码
A:初始化 README
    ↓
B:修改 README
    ↓
C:添加 notes/01-github-basics.md

在 main 指向 C 的情况下,可以通过提交历史查看 A、B、C。

GitHub 的仓库提交历史入口通常位于文件列表上方,显示类似:

复制代码
3 Commits

点击后可以浏览相关提交。

进入某次 Commit 详情,还可以查看这次提交的父提交信息及文件差异。

2. Diff 是什么?

Diff 是比较两个文件或两个版本之间差异得到的结果。

例如:

旧版本:

复制代码
int a = 10;

新版本:

复制代码
int a = 20;

差异可能显示为:

复制代码
-int a = 10;
+int a = 20;

GitHub 通常用红色表示删除内容、绿色表示新增内容。

但需要注意:

Diff 是比较结果,Commit 本身不能简单等同于增删行清单。

一次 Commit 仍对应完整文件树状态。

不同提交之间的 Diff,则帮助用户理解发生了哪些变化。

3. 仓库提交历史与单个文件历史的区别

假设:

复制代码
Commit A:创建 README
Commit B:修改 README
Commit C:创建 notes/git.md

那么:

查询对象 典型相关提交
整个 main 分支历史 A、B、C
README.md 路径历史 A、B
notes/git.md 路径历史 C

因为 Commit C 没有修改 README。

同样,在本次真实练习中,仓库文件列表曾显示:

复制代码
notes/       Add GitHub basics notes
README.md    Update learning notes

这是因为两条路径最近一次被修改的提交不同。

文件列表中的最近提交说明,不等于仓库整体的最新提交说明。

4. 文件路径历史与完整提交历史可能具有不同显示结果

GitHub 的 History 可以针对整个分支、某个文件或某个目录进行查询。

路径相关历史通常会过滤不影响指定路径的提交,并且可能进行历史简化。

因此,即使一个 Commit 可以从 main 的整体历史中访问,也不保证它会出现在某个文件的 History 页面中。

特别是 Merge Commit,可能因路径历史简化而没有显示在某个目录的 History 里。

这部分将在下篇结合实际 Merge Commit 例子完整推导。

八、第一部分知识关系

到这里,可以把上述概念组织成以下关系:

复制代码
GitHub
│
└── 托管 Git Repository
         │
         ├── Branch 引用
         │      └── 指向 Commit
         │
         └── Git 对象数据库
                │
                ├── Commit
                │    ├── parent → 父 Commit
                │    └── tree   → 根 Tree
                │
                ├── Tree
                │    ├── 文件名、模式、对象 ID
                │    ├── Blob 引用
                │    └── 子 Tree 引用
                │
                └── Blob
                     └── 文件内容字节

这个模型同时解释了:

  • 为什么不同提交可以定位不同项目版本。
  • 为什么一个提交可以关联到之前的历史提交。
  • 为什么 Git 不必每次完整复制全部文件。
  • 为什么文件路径与内容对象不是同一个东西。
  • 为什么删除或移动文件不会直接修改历史 Commit。
  • 为什么从旧提交仍可能恢复历史文件内容。

下篇继续从 Branch、HEAD 与提交之间的引用关系展开,并进入提交可达性、Detached HEAD、Pull Request、共同祖先与三方合并的完整机制。

相关推荐
miofly1 小时前
GitHub 日榜趋势速报 | 2026-10-24
开源·github
怕浪猫9 小时前
外包和大厂的前端代码,差在凌晨三点
前端·面试·github
徐小夕9 小时前
花了3个月,我们做了一款 AI 版的“飞书文档”
前端·算法·github
穆梓兰煊14 小时前
pgvector 0.8 评测:过滤向量检索的召回修复与边界
github
诚小纯14 小时前
qwen-deep-research 接入实录:两阶段调用、SSE 解析、报告结构与 35 处不可追溯的引用
人工智能·github
用户99484388107014 小时前
开源了一个把老 PHP 站点自动升级到 PHP 8.5 的工具(应对 PHP2Shell 漏洞)
github
Kimnol14 小时前
我的第一次 Git 贡献:从 fork 到合并 PR #3130
git
用户72737441388415 小时前
用 sha256 校验 Windows 下载文件:从 EZA Modern LS 说起
github
miofly15 小时前
Gemini 4 Argon发布,机器人ERQA推理得分87%但缺可比基准
开源·github