Git与GitHub系统学习|本地Git工作模型:终端与进程、CWD、HEAD/Index/工作树及Git对象

从 GitHub 网页端走向 Windows 本地 Git: 终端与进程、工作目录与仓库、HEAD/Index/Working Tree、Git 对象与文件树

文章目录

    • [一、为何从 GitHub 网页操作转向 Windows 本地 Git](#一、为何从 GitHub 网页操作转向 Windows 本地 Git)
    • [二、Git for Windows、Git Bash、终端、Shell、Git 程序、Windows:谁是谁](#二、Git for Windows、Git Bash、终端、Shell、Git 程序、Windows:谁是谁)
      • [2.1 Git for Windows 是什么](#2.1 Git for Windows 是什么)
      • [2.2 终端(Terminal)负责什么](#2.2 终端(Terminal)负责什么)
      • [2.3 Shell(命令解释器)负责什么](#2.3 Shell(命令解释器)负责什么)
      • [2.4 Git 是什么,Windows 又做什么](#2.4 Git 是什么,Windows 又做什么)
    • [三、程序与进程:一次 Git 命令实际上如何运行](#三、程序与进程:一次 Git 命令实际上如何运行)
    • [四、Windows 安装与安全环境检查:第六课 6.1 的原始操作](#四、Windows 安装与安全环境检查:第六课 6.1 的原始操作)
      • [4.1 检查 Git Bash、Git 版本](#4.1 检查 Git Bash、Git 版本)
      • [4.2 如果未安装,选哪个发行形式](#4.2 如果未安装,选哪个发行形式)
    • 五、当前工作目录(CWD)究竟是什么:不是"软件住的文件夹"
      • [5.1 `pwd`、`ls`、`cd` 与 `~`、`.`、`..`](#5.1 pwd、ls、cd 与 ~、.、..)
      • [5.2 为什么 Shell 需要 CWD,哪怕 Git 也能指定路径](#5.2 为什么 Shell 需要 CWD,哪怕 Git 也能指定路径)
    • [六、Shell CWD 与 Git 进程 CWD:继承、仓库发现、`git -C`](#六、Shell CWD 与 Git 进程 CWD:继承、仓库发现、git -C)
      • [6.1 Git 通常继承 Bash 启动时的目录](#6.1 Git 通常继承 Bash 启动时的目录)
      • [6.2 `git -C` 是谁处理的](#6.2 git -C 是谁处理的)
      • [6.3 "仓库发现"和"路径范围"必须分别判断](#6.3 “仓库发现”和“路径范围”必须分别判断)
    • [七、Git 工作树、Git 管理目录、Git 仓库、HEAD:四类常被混淆的对象](#七、Git 工作树、Git 管理目录、Git 仓库、HEAD:四类常被混淆的对象)
      • [7.1 新项目如何成为本地仓库(本批只是讲过含义)](#7.1 新项目如何成为本地仓库(本批只是讲过含义))
      • [7.2 为什么 `pwd` 与 Git 工作树根目录查询结果可能不同](#7.2 为什么 pwd 与 Git 工作树根目录查询结果可能不同)
    • 八、HEAD、Branch、Commit、Tree:一个分支为何能"指向版本"
    • [九、Git 四类核心对象与对象 ID:版本文件树存的究竟是什么](#九、Git 四类核心对象与对象 ID:版本文件树存的究竟是什么)
      • [9.1 Git 对象 ID 的内容寻址含义](#9.1 Git 对象 ID 的内容寻址含义)
      • [9.2 Blob 里有文件名、路径或下一级"数据对象引用"吗](#9.2 Blob 里有文件名、路径或下一级“数据对象引用”吗)
      • [9.3 Tree 条目到底是什么](#9.3 Tree 条目到底是什么)
      • [9.4 根 Tree、子 Tree 与 Blob 怎样还原路径](#9.4 根 Tree、子 Tree 与 Blob 怎样还原路径)
    • [十、Git 目录对象如何复用:同一个 Blob、同一个子 Tree、不同的根 Tree](#十、Git 目录对象如何复用:同一个 Blob、同一个子 Tree、不同的根 Tree)
      • [10.1 两个文件与一个 Blob](#10.1 两个文件与一个 Blob)
      • [10.2 两个不同的根 Tree,共享不变子目录](#10.2 两个不同的根 Tree,共享不变子目录)
      • [10.3 只修改 `src/Main.java`,为什么 Src 与 Root 都变](#10.3 只修改 src/Main.java,为什么 Src 与 Root 都变)
      • [10.4 不同根 Tree 给同一子 Tree 取不同名称](#10.4 不同根 Tree 给同一子 Tree 取不同名称)
      • [10.5 为什么不能无限重复生成"条目对象/变量引用对象"](#10.5 为什么不能无限重复生成“条目对象/变量引用对象”)
    • [十一、根 Tree 为什么不保存工作树根目录名?路径究竟来自哪里](#十一、根 Tree 为什么不保存工作树根目录名?路径究竟来自哪里)
    • [十二、Windows 文件和 Git Tree/Blob 并非实时一对一映射](#十二、Windows 文件和 Git Tree/Blob 并非实时一对一映射)
    • [十三、Index / Staging Area:为何既不是 Windows 工作树,也不是一堆历史 Commit](#十三、Index / Staging Area:为何既不是 Windows 工作树,也不是一堆历史 Commit)
      • [13.1 三个版本可以同时不同](#13.1 三个版本可以同时不同)
      • [13.2 为什么需要 `git add`,而不是改文件就等于暂存](#13.2 为什么需要 git add,而不是改文件就等于暂存)
      • [13.3 `git commit` 与不可变历史](#13.3 git commit 与不可变历史)
    • [十四、Index 只存 `docs/readme.md`,为什么足以重建确定的子 Tree 与根 Tree](#十四、Index 只存 docs/readme.md,为什么足以重建确定的子 Tree 与根 Tree)
      • [14.1 Index 的常规路径记录与 Git Tree 的逐层记录不同](#14.1 Index 的常规路径记录与 Git Tree 的逐层记录不同)
      • [14.2 哪些条件相同时才能确定相同 Tree](#14.2 哪些条件相同时才能确定相同 Tree)
    • [十五、空文件、空目录、空 Blob、空 Tree:格式允许性与普通跟踪规则的差异](#十五、空文件、空目录、空 Blob、空 Tree:格式允许性与普通跟踪规则的差异)
      • [15.1 为什么零字节文件能被跟踪](#15.1 为什么零字节文件能被跟踪)
      • [15.2 Git 也允许空 Tree,为什么普通 Git 不跟踪空目录](#15.2 Git 也允许空 Tree,为什么普通 Git 不跟踪空目录)
      • [15.3 嵌套空目录也不能自动跟踪](#15.3 嵌套空目录也不能自动跟踪)
      • [15.4 `.gitkeep` 如何保留整条路径](#15.4 .gitkeep 如何保留整条路径)
    • [十六、`git switch`、HEAD 与工作树:历史快照如何显示到 Windows](#十六、git switch、HEAD 与工作树:历史快照如何显示到 Windows)
    • 十七、用一个完整示例连接进程、路径、Tree/Blob、暂存和提交
      • [17.1 初始项目、仓库与目录关系](#17.1 初始项目、仓库与目录关系)
      • [17.2 提交 C1 的对象结构](#17.2 提交 C1 的对象结构)
      • [17.3 只改变 Main.java,产生 C2](#17.3 只改变 Main.java,产生 C2)
      • [17.4 再给 docs 改名](#17.4 再给 docs 改名)
      • [17.5 最后记录空目录层级](#17.5 最后记录空目录层级)
      • [17.6 隔离演示与真实操作证据的界限](#17.6 隔离演示与真实操作证据的界限)
    • 十八、重要概念误区的逐项辨析
    • 十九、命令清单:哪些在本批实际运行,哪些只是已解释的示例
    • 二十、复习时可以独立推演的九组问题

一、为何从 GitHub 网页操作转向 Windows 本地 Git

前五课已经在 GitHub 网站上操作过仓库 github-study-lab,接触 Repository、文件、Commit、Branch、HEAD、Pull Request,以及 Commit/Tree/Blob 对象和分支合并。因此第六课开始时,主要新增的是在哪里运行 Git、在本地管理什么数据、程序如何工作,而非重新把 GitHub 网站当作 Git 本体。

在 GitHub 页面编辑 README 的典型操作流程是:打开远程仓库 → 修改文件 → 在网站上形成提交 → 在网站中查看提交历史。这适合初学界面,但一个真实 Java 项目往往涉及 src/Main.java、src/User.java、pom.xml、README.md,需要 IDE 编辑、Maven 构建、运行和调试;这些活动主要发生在本机的实际文件系统。

本地 Git 可在不连接 GitHub 的情况下创建版本记录、检查差异、管理分支。之后再使用 GitHub 作为远程代码托管和多人协作平台。这里最基本的职责划分是:

  • Git:版本控制程序,可在本机管理提交、对象、引用和文件状态。
  • GitHub:托管远程 Git 仓库与协作工作流的服务。
  • IDE(如 IntelliJ IDEA):编写、运行、调试项目的开发环境;可以集成 Git,但不因此取代 Git 的版本管理概念。
  • GitHub Desktop:另一种调用 Git 相关功能的图形界面使用方式;它不属于 Bash Shell。

一个本地提交并不会自动推送到 GitHub;"提交到本地版本历史"和"推送到远程仓库"是后续需要分别学习的操作。本次 6.1 只做安全环境检查和工作模型教学,并没有对既有 github-study-lab 执行任何未经解释的本地推送或项目修改。

二、Git for Windows、Git Bash、终端、Shell、Git 程序、Windows:谁是谁

2.1 Git for Windows 是什么

Git for Windows 是在 Windows 上使用 Git 的一套发行版,通常不只是孤立的 git.exe。其发行内容一般包含 Git 程序、用于 Windows 的 Bash/类 Unix 工具、Git Bash 启动入口、配套运行环境等。安装器(Installer)是部署这套软件的一种方式;发行版、安装程序、安装后的某个可执行程序不能混作同一个对象。

"Git Bash"通常是这套发行版给用户提供的 Bash 命令行使用入口与环境的名称,而不是另一套与 Git 不兼容的版本控制系统。打开 Git Bash 时,常见环境是:终端程序(如 MinTTY)提供窗口与文字交互,Bash 作为 Shell 运行;后续需要时由 Bash 启动 Git。启动入口可能在完成启动后退出,因此不能假设长期存在一个独立"Git Bash 守护进程"夹在 Bash 和 Git 之间。

2.2 终端(Terminal)负责什么

现代图形操作系统里的终端一般指终端模拟器 :提供窗口、接收键盘输入、显示文本输出、滚动、复制粘贴等。它为使用文本命令的程序提供交互通道,但通常不需要理解 git commit 的业务含义。

Windows Terminal 是一种终端程序,可承载 PowerShell、CMD、Bash 等 Shell。老式 Windows 控制台可能由 Console Host 承载;因此点击 Windows"运行"输入 cmd 并出现黑窗口,不能仅凭颜色断定窗口一定由 Windows Terminal 提供。Git Bash 常见的 MinTTY 也是终端前端,不等于 Bash 命令语言。

2.3 Shell(命令解释器)负责什么

Shell 是解析用户命令、执行自身内建命令、启动外部程序、组织参数、管道及重定向的一类软件;Bash、PowerShell、CMD 是不同 Shell / 命令解释器的具体例子。Bash 既指一种 Shell 实现,也伴随自己的命令语言与脚本语法。

例如:

bash 复制代码
cd /d/Projects/JavaApp    # Bash 内建 cd:改变 Bash 当前目录
git --version             # 通常由 Bash 启动外部 Git 程序
java -version            # 若安装和定位正确,Bash 也可启动 Java

所以 Shell 不只供 Git 使用;Git 程序也不需要理解 Bash 的所有语法。Bash 中:

bash 复制代码
name=Alice
echo "$name"

而 PowerShell 中可写:

powershell 复制代码
$name = 'Alice'
Write-Output $name

两者 Shell 语言不同,仍可能调用同一个 git --version 并向 Git 传递相同参数。PowerShell 的管道更强调对象,Bash 中常见工具的管道多围绕字节或文本流;Git Bash 中 ls 通常是类 Unix 列表工具,PowerShell 的 ls 常是 Get-ChildItem 的别名。命令名字偶尔相同,不代表实现、路径语义或语法完全一致。

2.4 Git 是什么,Windows 又做什么

Git 程序 实现 Git 版本控制逻辑,接收 status、add、commit、log、--version 等参数,读取仓库数据、计算比较、写对象、更新引用以及在必要时操作文件系统。Windows 操作系统负责进程、文件系统、资源和权限等基础设施,Shell/Git 都通过它运行。

"哪个程序真正操作 Git 仓库?"需要按层次回答:终端提供输入输出,Bash 解析并启动命令,Git 程序执行具体 Git 版本管理逻辑,Windows 提供进程和文件系统支持;说"Git Bash 在操作仓库"只是对整体使用环境的宽泛描述。

三、程序与进程:一次 Git 命令实际上如何运行

程序 是可执行代码或软件;进程 是程序正在运行的一次实例。Bash 可持续运行并等待命令,而普通 Git 命令通常每次启动一个新的 Git 进程(可能再调用其他程序),完成任务后退出。

以 git --version 为例:

  1. 用户在终端窗口输入文字并按 Enter。
  2. 终端交互通道使输入可被 Bash 读取。
  3. Bash 解析 git 为要执行的程序,--version 为参数。
  4. Bash 依据环境配置(包括可执行文件路径搜索机制)定位 Git,向操作系统请求启动对应进程。
  5. Git 进程接收参数、输出版本信息,通常随后退出。
  6. 标准输出经进程输入输出与终端机制显示;Bash 继续等待下一条命令。

重要纠正:这不要求 Bash 逐字接收、理解 Git 所有输出再人工转发。Git 通常具有适当配置的标准输入、标准输出和标准错误通道。Shell 负责控制调用和组织流程,而具体 Git 输出可以通过相关通道到达终端。

普通命令序列:

bash 复制代码
git --version
git status
git log

通常意味着三次相继的 Git 命令执行及相应进程生命周期;不是一个必须永久驻留等待请求的 Git 主进程。"一次 Git 命令一个进程"只是常用教学模型:更复杂的操作可能启动辅助进程,IDE、GUI、服务端存在不同组织方式。

这也回答"Git 退出后如何记得上次提交":**持久版本数据保存在本地仓库,不保存在已经退出的 Git 进程状态中。**重新打开 Git Bash、重新启动 Windows,仍可读取以前写入的 Git 对象与引用。

四、Windows 安装与安全环境检查:第六课 6.1 的原始操作

4.1 检查 Git Bash、Git 版本

原课程先要求在 Windows 开始菜单查找 Git Bash;找到后打开,执行:

bash 复制代码
git --version

它检查当前命令行环境能否找到并运行 Git,通常不要求已经进入 Git 仓库、登录 GitHub、联网,也不会创建提交或修改分支。git 指程序,--version 是 Git 自己的选项。

典型提示符:

text 复制代码
user@PC MINGW64 ~
$ git --version
git version 2.x.y.windows.z
$
  • user@PC:常见的用户/计算机提示信息,可由配置定制。
  • MINGW64:常见 Git Bash 运行环境标识,并非当前 Git 仓库名。
  • ~:Bash 中常表示用户 Home Directory(主目录)。
  • $:普通用户 Shell 的常见输入提示符;不是输入命令时要敲的字符。
    本轮有真实用户截图证据,当时 Git Bash 输出:
text 复制代码
$ git --version
git version 2.45.2.windows.1
$ pwd
/c/Users/32868

这能证明当时 Git 在该 Shell 中可以运行,且执行 pwd 时 Bash 当前目录是 /c/Users/32868(通常对应 C:\Users\32868)。不能 由此推出该目录就是 Git 工作树,也不能推出用户实际运行过 git status、git init、git add 或 git commit。

4.2 如果未安装,选哪个发行形式

课程提及 Git for Windows 官方下载及不同设备架构:

  • x64:常见 Intel/AMD 的 64 位 Windows 平台。
  • ARM64:相应 ARM 架构 Windows 设备。
  • 可到 Windows 设置中的"系统 / 系统信息 / 系统类型"等处核对平台架构。
  • Standalone Installer:一般安装器,适合按向导部署并配置常规环境。
  • Portable :便携形式,用于需要非典型安装部署的场景。
    安装界面可能涉及安装路径、Git Bash / Git GUI 组件、PATH 中 Git 的调用方式、默认编辑器、HTTPS/SSH、凭据管理、跨操作系统换行符策略等。它们不全是外观选项,有些影响其他终端或现有项目。原课安排是先确认是否已安装;若没安装,依据真实界面判断设置,尤其不能在不知道原有环境时随意更改 PATH、认证或换行符策略。本批并无用户展示并完成这些配置的实操记录。

五、当前工作目录(CWD)究竟是什么:不是"软件住的文件夹"

Current Working Directory(CWD) 是进程的一项运行时目录状态:为解析相对路径和某些命令默认操作位置提供基准。它不是程序安装目录,也不是"哪个文件夹拥有这个进程"。

假设:

text 复制代码
Bash 程序可能安装于: C:\Program Files\Git\bin\bash.exe
Bash 当前工作目录: D:\Projects\JavaApp\src
Git 工作树根目录: D:\Projects\JavaApp

这三者可以同时不同。说"Bash 处在 src",只是人们对其 CWD 的简写,不意味着 bash.exe 被移动到 src;同样,目录不是 Bash 或 Git 进程私有的文件空间。

5.1 pwd、ls、cd 与 ~、.、..

bash 复制代码
pwd                        # 显示 Shell 当前工作目录
ls                         # 列出当前目录的常见非隐藏条目
ls -a                      # 包括以 . 开头的隐藏条目
cd /d/Projects/JavaApp     # 修改 Bash 当前目录(假设该目录存在)
cd src                     # 进入当前目录下的 src
cd ..                      # 进入父目录
cd ~                       # 返回用户主目录

在 Bash 中,pwd 通常是内建命令;cd 通常是内建命令。ls 一般在未指定路径时,以执行时的当前目录为默认操作对象。ls 程序安装在哪里与列出哪个目录不同;cd 也不移动 Git HEAD 或更改分支。

  • . 是路径语境中的当前目录;.. 是父目录,不是 Git 历史中的父 Commit。
  • ~ 是用户主目录的常见 Bash 表示,通常可展开为 $HOME。
  • Git Bash 中 /c/Users/... 与 Windows 中 C:\Users\... 可以指向同一个文件系统位置;/d/Projects/... 对应 D 盘路径。
    从 /d/Projects/JavaApp 执行 cd src 后,pwd 可显示 /d/Projects/JavaApp/src。工作树根目录如果仍为 JavaApp,并不会因 cd 变成 src。

5.2 为什么 Shell 需要 CWD,哪怕 Git 也能指定路径

Shell 除了执行 Git 还可执行 cat README.md、ls、java 等命令。未指定绝对路径时,程序需要相对路径的参照。例如 CWD 是 JavaApp,README.md 通常指根目录下的文件;CWD 改成 JavaApp/src,同名相对路径就通常指 src/README.md。CWD 只给出一个默认路径基准,不是安全权限边界:程序能否读写其他目录取决于实际路径、权限和命令设计。

六、Shell CWD 与 Git 进程 CWD:继承、仓库发现、git -C

6.1 Git 通常继承 Bash 启动时的目录

在常见 Git Bash 中,Bash 从 D:\Projects\JavaApp\src 执行 git status,新 Git 进程初始 CWD 通常也是 D:\Projects\JavaApp\src。这是两个进程各自持有、恰好路径相同的状态;并不需要把 git.exe 拷贝到那里。

Git 随后根据有效目录及环境配置定位仓库:普通仓库发现通常会检查当前目录并沿父目录寻找 .git 管理信息。因此即使从 JavaApp/src 启动,Git 通常也能找到 JavaApp 对应的仓库;Git 知道工作树根 并不等于必须先让 Bash 执行 cd ..。不同 Git 命令在读取或更改的文件范围上仍可能不同。

6.2 git -C 是谁处理的

bash 复制代码
git -C /d/Projects/JavaApp status

这里 -C 是 Git 的全局选项 ,语义相当于"按从指定目录启动 Git 的效果运行后面的 Git 命令"。即使父 Bash 进程 CWD 仍是 /c/Users/...,Git 也可以将目标上下文设置为 /d/Projects/JavaApp。命令结束后父 Bash 的 CWD 不会因此改变。对于相对路径形式的 -C,还需有可供解析它的原始路径上下文。-C 不是 Bash 自己悄悄替用户执行 cd 的别名。Git 还有 --git-dir、--work-tree 和相关环境配置,这些只是在当前阶段需知道存在的特例,不提前展开完整配置管理。

比较:

命令组合 Bash 执行之后的 CWD Git 有效操作位置
cd /d/Projects/JavaApp 后 git status JavaApp 默认从 JavaApp 定位仓库
在其他目录执行 git -C /d/Projects/JavaApp status 保持原 Bash 目录 Git 按从 JavaApp 启动的效果运行

6.3 "仓库发现"和"路径范围"必须分别判断

下面两个问题并不同义:

  1. Git 发现自己应操作的是哪个 Repository?
  2. 这条 Git 命令应检查或暂存该仓库中的哪些路径?

例如在 JavaApp/src 运行 git status,通常报告整个相关仓库的状态,不因为 Shell 位于 src 就自动只检查 src。但运行 git add .,点号作为路径参数通常把操作限制于当前 src 目录及其下方符合条件的变化。

bash 复制代码
# 假设当前位于 JavaApp/src
git add .              # 通常暂存 src/ 范围内的相关变化
git add -A             # 无路径参数:现代 Git 通常暂存整个工作树的相关变化
# 假设在工作树根目录
git add .              # 此时 . 的范围涵盖工作树根及下方

这里"相关变化"受已跟踪状态、忽略规则、路径匹配等影响,不是把所有东西无条件存进去。git add . 是一次根据当前文件状态更新 Index 的操作,不是持续监控文件;之后再修改文件,需要再次暂存新修改才能改变 Index 中准备的版本。

bash 复制代码
git -C /d/Projects/JavaApp/src add .

这个例子中的 . 按 Git 有效目录上下文落在 JavaApp/src,不会永久修改父 Shell 的目录。本文示例仅说明语义,并不意味着用户已经在真实项目上执行这些命令。

七、Git 工作树、Git 管理目录、Git 仓库、HEAD:四类常被混淆的对象

典型普通非裸仓库:

text 复制代码
JavaApp/                  ← 工作树根目录
├── .git/                 ← Git 管理数据
│   ├── HEAD
│   ├── objects/
│   ├── refs/
│   ├── index
│   └── ...
├── src/
│   └── Main.java
├── pom.xml
└── README.md
  • Working Tree(工作树):实际用于编辑、运行、构建项目文件的文件系统区域。可以含已跟踪、未跟踪、被忽略的文件;所以"位于工作树内"不保证"已提交"。
  • Git Directory(Git 管理目录) :普通仓库常见为 .git/,存放对象、引用、索引与其他元数据。这些管理文件通常不是 Commit 文件树中正常受跟踪的项目文件。
  • Git Repository(Git 仓库):严格内部讨论可指管理版本历史所需的存储及元数据,日常也常泛指整个受 Git 管理的 JavaApp 项目。语境不同不可把"仓库"等同于每个文件都已跟踪的 Windows 目录。
  • HEAD:Git 用于定位当前检出位置的特殊引用状态,不是进程,也没有自己的 CWD。

.git/objects 保存对象数据库中的松散对象等存储;对象也可能在 Packfile 中。.git/refs 可保存一些松散引用,其他引用可能保存在 packed-refs 或不同的 ref 后端。packed-refs 是引用存储机制,Packfile 是对象压缩/打包存储机制,不能混同。Index 常以 .git/index 保存暂存状态。

边界:在 linked worktree、子模块等情况下,工作树根的 .git 可能是指向实际管理位置的文件;Bare Repository 通常没有普通可编辑工作树;一个仓库还可关联多个 Worktree,某些状态按工作树分别保存。所以"每个 Git 仓库都在工作树里有一个 .git 目录"只适用于典型普通仓库模型,不是定义的必要条件。

7.1 新项目如何成为本地仓库(本批只是讲过含义)

在已有项目目录:

bash 复制代码
cd /d/Projects/JavaApp
git init

常见默认情况会建立 Git 管理数据,但不会自动创建第一个 Commit 。另一种方式 git clone <远程仓库地址> 可建立本地副本及相关仓库;完整 clone/认证/远程通信尚属后续课程。本批未有用户在 Windows 实际执行 git init 或 git clone 的证据。

7.2 为什么 pwd 与 Git 工作树根目录查询结果可能不同

bash 复制代码
pwd
git rev-parse --show-toplevel

前者显示 Bash 当前目录(比如 JavaApp/src);后者在 Git 能正确定位工作树时返回工作树根目录(比如 JavaApp)。若当前目录不在可发现的 Git 工作树内,pwd 仍可运行,而后者可能报错。

text 复制代码
fatal: not a git repository (or any of the parent directories): .git

这样的 git status 报错通常表示此次执行上下文下没有发现可操作仓库,不能直接推导 Git 未安装 ,也不能说 .git 就是 Shell 的当前目录。

八、HEAD、Branch、Commit、Tree:一个分支为何能"指向版本"

在普通分支检出状态:

text 复制代码
HEAD → refs/heads/main → Commit C
                              ├── parent → Commit B
                              └── tree   → Root Tree T

HEAD 通常保存类似 ref: refs/heads/main 的符号引用;refs/heads/main 是本地分支的逻辑引用名,目标是 Commit 的对象 ID。在松散引用存储中可能有 .git/refs/heads/main 文件,但名字与逻辑引用身份不能依赖它永远是一个独立文件。

Commit 是不可变的 Git 对象,保存根 Tree ID、一个或多个父 Commit ID、作者/提交信息等。普通创建新提交时,Git 创建新 Commit(假设 D 的父提交是 C),并移动当前 main 分支引用:

text 复制代码
D.parent = C
HEAD → main → D

原来的 C 不被改写。父提交引用方向指向已有历史提交;Git 的普通提交历史是有向无环图(DAG),不是修改旧提交的"子指针"。

Detached HEAD 时 HEAD 可直接指向 Commit,而不经普通分支;在刚 git init 尚无提交时,HEAD 也可能指向一个尚未建立实际提交目标的 unborn branch。因而"每个工作树必须始终有当前分支指向已存在 Commit"是错误的绝对化前提。HEAD/Branch/Commit 作为引用与对象本身并不是独立进程,不具有自己的当前工作目录。

九、Git 四类核心对象与对象 ID:版本文件树存的究竟是什么

Git 中常见四种对象类型:Blob、Tree、Commit、Tag。

对象 主要保存内容 不直接保存什么
Blob 一份文件内容的字节数据 固定文件名、Windows 路径
Tree 一层目录下各条目的模式、名称、目标对象 ID 自己被父目录叫作什么、固定绝对路径
Commit 根 Tree ID、父提交、作者与提交说明等 实际 Windows 工作目录的指针
Annotated Tag 被标注对象及类型、标签名、标注信息等 并非所有标签形式都要有 Tag 对象

9.1 Git 对象 ID 的内容寻址含义

Git 对象由其类型与内容形成标识:对象的规范哈希输入包含对象类型、内容长度、NUL 分隔符及实际数据内容。简化例子(不包含换行的 5 字节 Hello):

text 复制代码
blob 5\0Hello

这里 \0 表示一个数值为零的字节,不是字面斜杠和字符 0。Git 仓库可以采用 SHA-1 或 SHA-256 等对象格式,同一对象类型、同一序列化字节内容、同一对象格式下会计算出同一个 OID。文件后缀名不属于 Blob 的哈希输入,所以 A.txt 与 B.md 如果真实内容字节完全一样,能够引用同一个 Blob。可见文字同为 Hello 却一份附带换行 Hello\n,则字节不同,OID 通常不同。对象标识的"内容相同"始终需要明确是字节内容 。这里说的字节是 Git 最终记录进 Blob 的字节;在启用文本换行符规范化或其他内容过滤器时,工作树中的原始文件字节与写入 Blob 的字节可能有差异,不能忽略安装与仓库配置就把两者无条件等同。

对象 ID 不等同于 Java 的引用地址。Java 中两个不同 new Person("Tom") 实例即使属性一样,也未必同一对象;Git 的内容寻址使相同对象类型及规范内容得到同一逻辑对象 ID。多个位置能使用同一 OID 是有用的类比,但不能把 Git 的 OID 误解成内存中的 Java 指针或会随对象"原地编辑"而改变的变量地址。

9.2 Blob 里有文件名、路径或下一级"数据对象引用"吗

通常的普通文件 Blob 直接保存内容字节;它不需要再引用另一个所谓"真实文件内容对象",也不携带固定文件名 A.txt、src/Main.java 或 Windows 绝对路径。文件名与路径由 Tree 的层级构造。

例:

text 复制代码
Tree T
├── A.txt → H_X
└── B.txt → H_X

Blob X(OID H_X)
内容:Hello

两条 Tree 记录名称不同,但都能定位同一个逻辑 Blob X。此处"复用"是共享内容寻址对象,不是把两条 Tree 记录合并成同一条,也不需要再出现名叫"条目引用对象"的额外 Git 对象类型。

9.3 Tree 条目到底是什么

对普通文件、目录而言,一条 Tree 记录包含:

text 复制代码
mode = 100644
name = A.txt
oid  = H_X

另一个子目录条目:

text 复制代码
mode = 040000
name = docs
oid  = H_Docs

典型 mode:100644 普通不可执行文件,100755 普通可执行文件,040000(常见显示形式)目录,120000 符号链接,160000 子模块 gitlink。普通文件条目和符号链接一般指向 Blob,目录指向 Tree,gitlink 则含特殊 Commit 对象关联。mode、name、oid 是同一条 Tree 记录的三个不同组成字段;OID 标识目标对象,名称和模式不由这个 OID 字段自动包含。

一条普通 Tree 记录不是一个独立的 Git Object:它没有被单独赋予普通 Blob/Tree/Commit/Tag 式的对象 ID。Tree 对象自身存着全部条目数据,而 Tree 具有自己的 OID。别把"Tree 条目""Tree 对象""条目所指向的目标对象"当作同一个实体。

9.4 根 Tree、子 Tree 与 Blob 怎样还原路径

text 复制代码
Commit C
  └── Root Tree T
       ├── A.txt → Blob A
       └── docs → Tree D
                    └── help.md → Blob H

由 Commit 的 tree 字段直接定位根 Tree T;从 T 找 docs 的子 Tree ID,再到 D 找 help.md 的 Blob ID,最后读取文件内容。由沿途名称构造 docs/help.md。根 Tree 只直接保存第一层条目,不必把所有后代文件条目冗余地存入根 Tree。Git 对象数据库中的根 Tree 不是指向 Windows 项目文件夹的实时指针 ,更不包含正常的 .git 管理目录,故不会形成"Root Tree → Windows 文件夹 → .git/HEAD → Commit → Root Tree"的错误循环。

Tag 补充:轻量标签 可直接通过 refs/tags/v1.0 → Commit C 这样的命名引用表示;附注标签一般由标签引用指向独立 Tag 对象,Tag 对象又记录所指的 Commit 或其他对象、标签名、创建者与消息。Tag 对象能引用其他 Git 对象,并不代表其内部必须再嵌套一个 Tag 对象,更不意味着无限自我引用。

十、Git 目录对象如何复用:同一个 Blob、同一个子 Tree、不同的根 Tree

10.1 两个文件与一个 Blob

假设一个版本的项目有:

text 复制代码
Project/
├── A.txt  内容:Hello
└── B.txt  内容:Hello

根 Tree 可以保存两个不同的条目:

text 复制代码
100644 A.txt → Blob X(H_X)
100644 B.txt → Blob X(H_X)

一个文件名称/路径身份 与它所引用的内容对象身份是两条独立的维度。A、B 两个路径不同,不妨碍它们共享一个 Blob。若仅改变 B.txt 的文件名,Blob X 无须改变,含该名称的父 Tree 内容则变化。

10.2 两个不同的根 Tree,共享不变子目录

text 复制代码
Commit C1 → Root Tree T1
  ├── A.txt → Blob A1
  ├── docs  → Tree Docs
  └── src   → Tree Src1

Commit C2 → Root Tree T2
  ├── A.txt → Blob A1
  ├── docs  → Tree Docs
  └── src   → Tree Src2

这里 A.txt 和 docs 没变,因此新旧根 Tree 可共同引用原 Blob A1 和 Tree Docs。但 src 的内容变化了,于是需要 Tree Src2。根 Tree 中相应 src 条目所记录的对象 ID 改变,因此 Root Tree T2 与 T1 不同。

注意两份根 Tree 中相同的 A.txt、docs 普通条目数据 各自属于所在 Tree 的内容;Git 没有自动把这段条目分离成独立"条目对象"供二者引用。被复用的是它们的目标对象。若两份整个根 Tree 的序列化内容都完全相同,则可以直接让两个 Commit 共享一个根 Tree ID,而无须两份不同 Tree。

10.3 只修改 src/Main.java,为什么 Src 与 Root 都变

原版本:

text 复制代码
Root T1:src → Tree Src1
Tree Src1:Main.java → Blob Main1
Blob Main1:Version1

工作树中编辑成 Version2,并在记录新版本的流程中写出新 Blob Main2:

text 复制代码
Blob Main2:Version2

为了记录新文件版本,需要一个条目:

text 复制代码
Main.java → Blob Main2

但旧 Tree Src1 内容不可原地编辑,否则其内容寻址 ID 与旧版本所引用的对象身份不再一致。因此形成新 Tree Src2:

text 复制代码
Tree Src2:Main.java → Blob Main2

相应地,旧 Root T1 仍记录 src → Tree Src1;要记录新目录状态,形成新 Root T2,记录 src → Tree Src2。这条从变化的 Blob → 直接父 Tree → 祖先 Tree → 根 Tree的变化链很重要。

text 复制代码
Commit C1                  Commit C2
  └ Root T1                  └ Root T2
       ├ docs → Tree D           ├ docs → Tree D  (共用)
       └ src → Tree Src1         └ src → Tree Src2
                     │                       │
                     └ Main1                 └ Main2

只有受变化影响的祖先 Tree 需要采用新对象内容;未变化的其他子树可继续引用原有对象。Tree 可复用的条件不是"里面的文件名没变",而是整个 Tree 对象内容(包括条目名称、mode、OID 和格式)没有变化。 即使文件字节不变,如果可执行模式等被记录的信息改变,相应 Tree 也可能变。

10.4 不同根 Tree 给同一子 Tree 取不同名称

text 复制代码
Root T1:docs    → Tree D(H_D)
Root T2:manuals → Tree D(H_D)

Tree D 内部如果仍为:

text 复制代码
B.txt → Blob Y
C.txt → Blob Z

那么外部名称 docs / manuals 由父 Tree 的条目 决定,Tree D 不保存"我一定叫 docs"或"我的父亲一定是 T1"。因此 Tree D 自己 ID 不变,T1/T2 内容因条目名称不同而不同。同一个根 Tree也可以同时含:

text 复制代码
docs    → Tree D
manuals → Tree D

使两个不同项目路径共享同一子 Tree。注意:若改变的是 Tree D 自己内部的 B.txt 名称,Tree D 内容就变了,不能继续无条件使用原 ID。

10.5 为什么不能无限重复生成"条目对象/变量引用对象"

Git 正式逻辑对象的边界已规定:Blob 保存字节、Tree 保存一组条目、Commit 保存提交元信息及引用、Tag 保存标签元信息。name + mode + oid 是 Tree 对象内部的数据,并不是另一种自带 OID 的普通 Git 对象。所以当两个 Tree 各自含 A.txt → H_X 时,Git 可以复用 Blob X;它不会自动再创建"共享的 A.txt 条目对象 E",令两个 Tree 改为引用 E。底层 Packfile 可能压缩相似数据,但这是另一层物理存储优化,不能替换逻辑对象模型。对象 ID 不会自动产生自我指向;一个 Tree 的 ID 也不因其被别人引用就写回 Tree 内容。

十一、根 Tree 为什么不保存工作树根目录名?路径究竟来自哪里

假设一个人把 Git 仓库检出在:

text 复制代码
D:\Projects\JavaApp

另一个人检出到:

text 复制代码
C:\Work\Demo

若两人指向同一个 Commit C,根 Tree T 可以完全相同:

text 复制代码
Commit C 的 tree = T
Root Tree T
├── A.txt → Blob A
└── docs  → Tree D
              └── B.txt → Blob B

Commit C 直接保存 T 的 ID;没有"父 Tree"还需要为 Root Tree 命名 。JavaApp 和 Demo 是各自 Windows 工作树根目录的位置/名称,通常不是 Commit 中需要固定保存的内容。因此,根 Tree 自身不存"我的外层目录名称是 JavaApp"。

内部子目录情况不同:要从根 Tree 找到 Tree D,根 Tree 必须有条目 docs → H_D,由这个父条目给出名称和目标 OID。再由 D 的 B.txt → H_B 条目确定下一级名称。逐层拼接,得到相对于根目录的完整路径:

text 复制代码
docs + / + B.txt = docs/B.txt

正常完整检出的目标路径可以按:

text 复制代码
Windows 工作树根目录 + Git Tree 中形成的相对路径

例如分别得到 D:\Projects\JavaApp\docs\B.txt、C:\Work\Demo\docs\B.txt。根目录绝对路径不同,项目内部的相对路径保持对应。

重要纠错:docs/B.txt 与 docs/C.txt 是两个不同的相对路径;即使二者内容 Blob ID 相同,也不能断言"Git 条目对应的 Windows 文件名可随意不同而相对路径不变",因为文件名本身就是相对路径的一部分。实际在资源管理器把 B.txt 改名为 C.txt,会产生当前工作树与旧 Commit 的差异,旧 Commit 中的 Tree 记录不会自动改写;正确暂存并形成新版本,才会产生新的相对路径记录。Git 可以识别/显示重命名,但这不是让旧 Commit 原地变成新路径。

检出边界:目标文件系统可能不支持某些特殊名称、路径或大小写组合。Windows 常见目录可能大小写不敏感,Git Tree 可包含只有大小写差异的两个名称;此时完整检出可能存在限制或冲突。Sparse Checkout(稀疏检出)可以有意只呈现文件树的一部分。这些条件限制实际呈现,并不意味着对象 ID 改变或 Git 将不同名称视作同一个相对路径。

十二、Windows 文件和 Git Tree/Blob 并非实时一对一映射

同一个名字 Main.java 可能指:

  1. Windows 工作树里当前真实的 src/Main.java 文件,用于编辑、编译与运行;
  2. 某个 Git Tree 中名称为 Main.java 的条目,保存 mode 与 Blob OID;
  3. 该条目所引用的 Blob,保存当次记录的文件内容字节。

这三个层次可以对应同一文件路径,但不是同一个对象。一个 Blob 可以在不同提交、不同路径下共用;一个子 Tree 可以被不同父 Tree/不同名字引用;Windows 当前文件内容可被用户独立修改,旧 Commit/Tree/Blob 仍然保存原版本。

Windows NTFS 等文件系统自身也有目录与文件的元数据/数据组织和链接机制,因此不能粗糙说"Windows 绝不使用引用,而 Git 才用引用"。本课的有效区别是:Git 把版本的内容与路径结构明确保存为内容寻址的 Blob、Tree、Commit;Windows 工作树则呈现当前可读写文件的实际状态。

两个特殊但重要的例子:

  • A.txt 和 B.txt 内容完全相同,Windows 中可有两个独立可编辑的文件,而某个 Git Tree 中它们可共享同一个 Blob。
  • 工作树中 Main.java 已改成 Version2,但 HEAD 仍引用 Version1 的旧提交;Git 历史 Blob 不会随 Windows 编辑而实时更新。

十三、Index / Staging Area:为何既不是 Windows 工作树,也不是一堆历史 Commit

**Index(索引/暂存区)**通常表示当前为下一次提交准备的文件状态。它不只是"尚未提交的修改清单",还可包含完全未改变文件的路径、mode、OID 等记录。常规已检出仓库中,可以把 HEAD、Index、Working Tree 理解为三个可比较的状态来源:

  • HEAD:当前被检出的 Commit 所记录的版本树(在有正常提交的条件下)。
  • Index:为下一次提交准备的路径与对象状态。
  • Working Tree:Windows 文件系统中目前实际呈现、可继续编辑的文件。

13.1 三个版本可以同时不同

初始:

位置 A.txt B.txt
HEAD(Commit C) Version1 Version1
Index Version1 Version1
Working Tree Version1 Version1

用户用 IDE 修改 A.txt 到 Version2,但尚未暂存:

位置 A.txt B.txt
HEAD(Commit C) Version1 Version1
Index Version1 Version1
Working Tree Version2 Version1

执行 git add A.txt 后:

位置 A.txt B.txt
HEAD(Commit C) Version1 Version1
Index Version2 Version1
Working Tree Version2 Version1

再次在 IDE 中将 A.txt 改成 Version3,没有再次暂存:

位置 A.txt B.txt
HEAD(Commit C) Version1 Version1
Index Version2 Version1
Working Tree Version3 Version1

因此,正常 git status 可同时观察 HEAD→Index 的已暂存差异与 Index→Working Tree 的未暂存差异,另可报告未跟踪等文件状态。它不是简单比较三个"最新 Commit";Index、Working Tree 不是各自已经完成的正式提交。

13.2 为什么需要 git add,而不是改文件就等于暂存

git add A.txt 在正常文件状态下读取工作树该文件的内容,确保对应 Blob 能被对象数据库使用,并更新 Index 中 A.txt 的状态。之后再次修改工作树时,Index 不会自动跟着变,除非执行另一次适当的暂存操作。如此可以选择下一次提交记录哪一个内容状态。若只再次将 Version2 暂存为 Version3,常规 Index 中对应路径准备状态更新为 Version3,它不是按时间保存多次暂存完整历史的仓库。此前产生的 Blob 对象也未必立即物理删除,是否有引用以及后来何时回收属于另一层对象可达性和维护问题。

Index 在冲突合并等场景还可以有多个阶段(stage)的路径记录,不能把"每个路径永远只有一个 Index 版本"说成绝对规则;这是以后正式合并实践的特殊情况。本篇所有普通三态表以无未解决合并冲突的普通 Index 为前提。

13.3 git commit 与不可变历史

从 Index 构造下一次版本文件树,形成新的 Commit D:

text 复制代码
D.parent = C
D.tree = 根Tree新ID
HEAD → main → D

旧 Commit C、它的 Root Tree 与 Blob 仍然按原 ID 保存原历史状态,不会为了新提交将 C 的 Main.java 内容原地改成 Version2 。git add 更新 Index 不会自动产生 Commit;git commit 才会在满足条件时生成新提交并更新当前检出引用(典型在普通分支上移动分支指针)。

十四、Index 只存 docs/readme.md,为什么足以重建确定的子 Tree 与根 Tree

这是第七、第八轮追问中最集中的新机制。

14.1 Index 的常规路径记录与 Git Tree 的逐层记录不同

假设 Index 已有:

text 复制代码
100644 A.txt          → H_A
100644 docs/readme.md → H_R

这里路径相对工作树根目录,不需要再有一条独立的:

text 复制代码
docs → H_D

来表示普通目录。Index 可以直接存完整相对文件路径、文件模式与 Blob OID 等信息;实际 Index 还包含统计信息等用于状态检查的数据,但它们不都决定提交 Tree 的内容。

从 docs/readme.md 能明确解析:中间目录名 docs、最终文件名 readme.md。要产生提交中的 Tree 结构,Git 在逻辑上可这样推导(对象依赖顺序,不声称实现代码必须固定用这种遍历顺序):

  1. 由 readme.md / 100644 / H_R 确定 Tree D 的文件条目。
  2. 按 Git 规定的格式及排序,序列化 Tree D,计算其 OID H_D。
  3. 根 Tree 中写入 docs / 040000 / H_D;另有 A.txt / 100644 / H_A。
  4. 序列化根 Tree,计算根 Tree ID H_T。
text 复制代码
Index(扁平完整路径)
  A.txt          → H_A
  docs/readme.md → H_R
        │
        ▼
Tree D
  readme.md → H_R
  ID = H_D
        │
        ▼
Root Tree T
  A.txt → H_A
  docs  → H_D
  ID = H_T

所以"没在 Index 提前存 docs Tree ID 会导致每次随机形成不同子 Tree"的担忧不成立。Git 不是随机挑选 ID:在相同仓库对象格式下,相同条目数据经规定编码和排序得到相同对象内容,进而得到相同 OID。父 Tree 由子 Tree 的相同 OID 与相同名称/mode 组成,也保持相同。

14.2 哪些条件相同时才能确定相同 Tree

不能只说"文件名相同就行"。对于 Tree,决定对象内容的还有各条目的 mode、OID、名称和格式。例如:

text 复制代码
第一次:docs/readme.md → H_R   mode=100644
第二次:docs/readme.md → H_R2  mode=100644

文件名不变,Blob ID 变了,Tree D 通常也变,继而根 Tree 变。即使 Blob ID 不变,mode 从 100644 改为 100755,也可能形成不同 Tree。反之,工作树后来又改了但未重新暂存,只要当前 Index 的有效 Tree 内容未变,写出的 Tree 仍可相同。

git write-tree 的定义就是根据当前 Index 构造 Tree 对象;此命令在本批被用于理解/隔离演示,不是用户已经完成 git write-tree 实操的证据。实际 Index 要能处于适当一致、可写出状态;在未解决合并冲突时存在限制。

十五、空文件、空目录、空 Blob、空 Tree:格式允许性与普通跟踪规则的差异

15.1 为什么零字节文件能被跟踪

text 复制代码
Project/
└── empty.txt  (0字节)

这是 Windows 中存在的实际文件,尽管内容字节长度为零。正常暂存后,Tree 可以有条目:

text 复制代码
100644 empty.txt → Blob Empty

Blob Empty 有效,具有内容长度 0 的对象内容。在 SHA-1 格式仓库中,空 Blob 的完整对象 ID 通常为:

text 复制代码
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

零字节文件≠不存在文件:名称、mode、OID 仍能组成一个实际文件条目。

15.2 Git 也允许空 Tree,为什么普通 Git 不跟踪空目录

一个零条目的 Tree 在 Git 对象格式中同样合法。SHA-1 格式的空 Tree ID 为:

text 复制代码
4b825dc642cb6eb9a060e54bf8d69288fbee4904

但:

text 复制代码
Project/
└── empty-folder/

普通 git add 并不会仅因为 Windows 存在这个空目录,就在 Index 登记一个可提交的独立空目录路径;根据 Index 构造的常规 Tree 也就通常没有 empty-folder。所以不能从"普通 Git 不跟踪空目录"推导出"Git 对象格式禁止空 Tree"。这是格式能表示什么 和普通暂存流程实际记录什么的区别。

15.3 嵌套空目录也不能自动跟踪

text 复制代码
Project/
└── empty-folder/
    └── docs2/

Windows 意义上外层 empty-folder 确实有一个目录条目 docs2,但普通 Index 中没有 empty-folder/docs2/某个受跟踪文件 这样的有效路径。Git 不会因为 Windows 中有两层目录就自动写出:

text 复制代码
Root T → empty-folder / Tree E → docs2 / Empty Tree

这条对象结构可以在底层对象模型中表达 ,但不是普通 git add 根据空目录自动创建的结构。没有相关路径被普通 Index 记录,Git 便无从按常规规则知道应把一个合法空 Tree 挂到哪个目录名称和位置。

15.4 .gitkeep 如何保留整条路径

在最深层添加实际存在的零字节文件:

text 复制代码
Project/
└── empty-folder/
    └── docs2/
        └── .gitkeep

若此文件未被忽略且可正常跟踪,Index 可记录:

text 复制代码
100644 empty-folder/docs2/.gitkeep → Blob Empty

构造 Tree 时:

text 复制代码
Root T
  └── empty-folder → Tree E
        └── docs2 → Tree D2
             └── .gitkeep → Blob Empty

因此两个祖先目录都能在版本文件树中出现,正常检出时可重建层级。此时最里面的 docs2 不再是完全没有目录项的空目录,因为 .gitkeep 作为文件本身存在。.gitkeep 只是社区常用命名,不是 Git 规定的特殊语义文件。

如果只在 empty-folder 直接加 .gitkeep,而 docs2 仍空,通常只能记录外层 empty-folder,不会自动让独立空的 docs2 进入普通提交。

此处同样说明为何 Root Tree 不需要直接保存所有后代:通过正常 Index 中的完整受跟踪路径构造每层必要 Tree,而没有相应可跟踪路径的目录层级不被自动引入。

十六、git switch、HEAD 与工作树:历史快照如何显示到 Windows

Git 的某个 Commit 中的根 Tree 是对象数据库里的历史文件树状态 ,不是当前 Windows 工作树文件夹的实时指针。要让 Windows 工作树呈现目标版本,Git 在适当切换操作中读取目标 Commit/Tree/Blob,并更新需要变化的实际工作树文件及对应 Index 状态。

例如:

text 复制代码
main → Commit D:src/Main.java = Version2
old  → Commit C:src/Main.java = Version1
HEAD → main

在工作树与 Index 没有妨碍切换的本地修改时,执行:

bash 复制代码
git switch old

成功后:

text 复制代码
HEAD → old → C
Working Tree:src/Main.java = Version1

这不要求操作系统为每个 Commit 预先保存一套独立可编辑项目目录,也不表示 git switch 必然"删除并重新复制所有文件"。Git 可按需调整发生差异的文件。Tree 和 Blob 对象仍留在对象数据库中;工作树里是用于用户继续编辑、编译和运行的文件。

安全前提 :如果当前工作树 Main.java 已被修改到 Version3 且未提交,目标分支需要改变该文件,切换可能造成覆盖风险,Git 通常拒绝此类会丢失本地变更的切换;不冲突的本地修改在某些情况下可保留。切换不是无条件强制覆盖。危险的强制丢弃修改选项不属于本次已完成实践。git switch --detach <commit> 可在 Detached HEAD 状态呈现某个历史提交,无须为其事先创建普通分支;初始仓库还可能处于 unborn branch 状态。

"工作树一定等于 HEAD 提交"是错误的绝对说法。HEAD 提供检出版本的基准,工作树可随时被 IDE 编辑,Index 也可能与两者不同;只有在没有待处理差异的普通干净状态,它们才可一致。

十七、用一个完整示例连接进程、路径、Tree/Blob、暂存和提交

以下均为假设演示,不是用户电脑已执行记录。

17.1 初始项目、仓库与目录关系

text 复制代码
D:\Projects\JavaApp\
├── .git\
├── A.txt                 (Hello)
├── B.txt                 (Hello)
├── docs\
│   └── help.md           (Help)
├── src\
│   └── Main.java         (Version1)
└── empty-folder\
    └── docs2\           (无文件)

若 Shell CWD 是 JavaApp/src:

bash 复制代码
pwd                        # /d/Projects/JavaApp/src
git rev-parse --show-toplevel # 可为 D:/Projects/JavaApp
git status                 # 对 JavaApp 仓库检查状态

Shell 可以停留在 src,而实际 Git 工作树根仍为 JavaApp;.git 保存管理数据;git.exe 可安装在别处。pwd、git rev-parse、git status 查询的是不同对象或状态。

17.2 提交 C1 的对象结构

假设按普通方式暂存并提交 A.txt、B.txt、docs/help.md 和 src/Main.java,忽略两个空目录:

text 复制代码
Commit C1
└── Root Tree T1
     ├── A.txt → Blob X(Hello)
     ├── B.txt → Blob X(Hello)      ← 同一Blob复用
     ├── docs → Tree Docs
     │     └── help.md → Blob Help
     └── src → Tree Src1
           └── Main.java → Blob Main1(Version1)
  • A、B 两个不同文件条目共用一个 Blob X。
  • docs 是根 Tree 的子目录条目,Tree Docs 内只有自己的内容条目。
  • empty-folder/docs2 没有被普通 Index 记录,正常提交文件树中不出现。
  • Root Tree 不保存"JavaApp"这个工作树根目录名称。

17.3 只改变 Main.java,产生 C2

把工作树 Main.java 改为 Version2,并正常暂存后,Index 准备新 Blob Main2;依次形成 Tree Src2、Root T2,Commit C2 以 C1 为父提交:

text 复制代码
Commit C1                   Commit C2
   └ Root T1                   └ Root T2
     ├ A.txt → Blob X            ├ A.txt → Blob X
     ├ B.txt → Blob X            ├ B.txt → Blob X
     ├ docs  → Tree Docs         ├ docs  → Tree Docs
     └ src   → Tree Src1         └ src   → Tree Src2
                     └ Main1                     └ Main2

两次根 Tree 不同;相同 Blob X 与 Tree Docs 复用。此时若 HEAD 仍 C1 而 Index 已准备 Version2,git status 能报告待提交修改;正常创建 C2 后当前 main 移动到 C2,C1 保留其旧状态。

17.4 再给 docs 改名

如果将 docs/help.md 正式记录为 manuals/help.md,子 Tree Docs 内的 help.md 内容未变,它仍可复用;根 Tree 条目从 docs → Tree Docs 改为 manuals → Tree Docs,根 Tree 因名称变化而不同。docs/help.md 和 manuals/help.md 是不同相对路径,即使 Blob 和子 Tree 完全一样。

17.5 最后记录空目录层级

如果需要在版本中保留 empty-folder/docs2/,可加入可跟踪的 .gitkeep。Index 记录的是 empty-folder/docs2/.gitkeep 这一完整相对路径,按路径生成两层祖先 Tree;并非因为 Git 在 Windows 上看到两个空目录就自动把它们变成可提交目录对象。

17.6 隔离演示与真实操作证据的界限

前几轮助手曾报告在独立临时 Git 仓库 演示了 A/B 相同内容复用 Blob、未变化 docs Tree 复用,以及只修改 Main.java 时 src Tree、根 Tree 变化,例示短 OID 如 ce01362...、ba20bcf...、cb85daa... → e227bbe...、2376f84... → 8608780...;另一轮还报告用临时 Git 2.47.3 环境验证空 Tree、空 Blob 和 .gitkeep 行为。这些属于当时助手给出的隔离测试记录,不能写成用户 Windows Git 2.45.2 的个人实践结果。本次文章只是归档当时已说明的证据,不以重新运行这些实验或新增实际结果自居。

十八、重要概念误区的逐项辨析

容易出现的暂时理解 必须建立的正确机制
Git Bash 就是 Git 程序,终端也是 Shell Git Bash 是使用环境/入口;终端负责交互显示,Bash 解析执行,Git 完成版本逻辑
Git Bash 中的 ~ 是 Git 仓库目录 常是 Bash 用户主目录的显示方式,不代表 Git Repository 或工作树根
pwd 显示 bash.exe 所在路径 pwd 显示进程当前工作目录,安装位置可完全不同
Bash 的 CWD 就是 Git Working Tree 两者可相同、可在工作树内外、也可不同;CWD 是进程状态,Working Tree 是文件区域
运行一次 Git 命令后 Git 进程要一直存活保存历史 普通命令进程可结束;对象与引用持久化在仓库
cd 会移动 HEAD 或分支 cd 主要改变 Bash CWD;以后 Git 命令可能因继承 CWD 改变仓库定位
git -C 必须由 Bash 先切目录 -C 是 Git 处理的目录上下文选项;父 Bash 可保持原 CWD
git status 只看当前子目录 默认可检查仓库整体状态;带路径的 git add . 可被 CWD 限定
.git 就是工作树全部项目文件 .git 是管理数据,普通工作树可有未跟踪/忽略文件;仓库还有裸仓库等形式
HEAD、Branch、Commit 各有自己的工作目录 它们是引用或 Git 对象,非独立运行的进程
Commit 根 Tree 指向 Windows 真实目录 Commit 保存 Git 根 Tree OID,读取的是版本对象数据库,不是实时 Windows 路径指针
Git 对象库中有叫 A.txt 的专用内容对象,Blob 再指向"真实数据对象" A.txt 是父 Tree 中的名称字段;Blob 本身保存内容字节
Tree 条目是独立 Git 对象 条目是 Tree 内的 mode/name/oid 数据记录;完整 Tree 才是 Git 对象
文件后缀不同一定生成不同 Blob Blob OID 取决于类型与实际字节内容等规范输入;文件名/后缀不在普通 Blob 内
两个文件同 Blob 就只剩一个文件条目 两个不同路径仍需不同 Tree 条目,只是目标 OID 相同
新提交可以直接修改旧 Tree 内的 Main.java 引用 旧对象不可变;需要形成新 Blob、相应新祖先 Tree;旧提交保留
子 Tree 在另一位置更名后自身 ID 必变 外部名称由父 Tree 条目保存;子 Tree 内部不变时 OID 可不变
同一个 Blob 可被放到任意工作树路径且仍算原提交路径没变 对象复用与路径身份分离;文件名是相对路径的一部分,路径改变意味着 Tree 条目变化
Index 只列"修改过的文件" 它通常保存准备提交的整个文件路径状态,包括未变化的受跟踪文件
Index 不存 docs 的独立 Tree ID,就无法重复生成同一个 Tree 完整相对路径、mode、OID 决定分层 Tree 内容;确定性序列化与哈希得到相同对象 ID
空目录不跟踪,所以空 Tree 对象不合法 空 Tree 允许;普通 git add 不把独立空目录登记为可提交的文件路径
empty-folder 里有一个空 docs2 子目录,所以外层一定会被跟踪 没有普通 Index 路径包含此目录层级时,普通流程仍不会自动记录
.gitkeep 是 Git 必须识别的特殊格式 它只是普通可跟踪文件的习惯名称,靠文件路径使目录结构进入版本 Tree
HEAD 指向 C,实际文件就不能偏离 C 工作树可以继续修改,Index 可以准备不同内容;旧 C 不被修改
git switch 必须完整复制所有文件且总能覆盖本地修改 Git 按需更新工作树/Index,并对有丢失风险的本地变更作保护

十九、命令清单:哪些在本批实际运行,哪些只是已解释的示例

命令 本篇解释的含义 个人实际执行证据
git --version 检查 Git 是否能运行,不需要仓库 有,截图返回 2.45.2.windows.1
pwd 显示 Bash 当前工作目录 有 ,截图返回 /c/Users/32868
ls / ls -a 列目录条目;-a 包含以点开头的名称 仅讲解,未见用户本批执行证据
cd / cd .. / cd ~ 更改 Bash 当前目录 仅讲解,未见用户本批执行证据
git status 检查仓库状态、待提交与未暂存变化等 仅教学示例
git rev-parse --show-toplevel 查询有效 Git 工作树的根目录 仅教学示例
git -C <path> status 不先改父 Bash CWD,指定 Git 执行上下文 仅教学示例
git add . / git add -A 按路径范围或整个工作树更新 Index 仅教学示例
git init 在指定项目位置初始化本地仓库 仅提及机制,尚未真实练习
git commit -m ... 基于当前已暂存内容形成新提交 仅用于机制演示,尚未实操
git switch <branch> / --detach 切换分支或检出提交,必要时更新 Index 和工作树 仅用于理解,尚未实操
git write-tree 根据当前 Index 生成 Tree 对象 原追问含隔离环境示例,非用户实操
git clone、fetch、pull、push 本地和远程关系的后续命令 第六课 6.1 未正式实操,不从本篇推定掌握

在真实仓库运行命令前,必须先核对所在位置和文件变化风险。特别是后续强制检出、覆盖工作树、撤销暂存等操作,其安全规则不由本篇示例自动替代。

二十、复习时可以独立推演的九组问题

  1. Git for Windows、Git Bash、Bash、MinTTY、git.exe、GitHub、Windows 各是什么对象?从键盘敲入 git --version 到输出的因果步骤有哪些?
  2. pwd 返回 JavaApp/src 而 git rev-parse --show-toplevel 返回 JavaApp 时,分别指哪个对象?与 bash.exe 的安装目录有什么区别?
  3. 如果在 JavaApp/src 下执行 git status 和 git add .,为什么两者可能涉及不同路径范围?git -C 修改谁的执行上下文?
  4. HEAD、main、Commit C、根 Tree、src 子 Tree、Blob Main1 之间每一条引用由谁存储?哪些是进程,哪些是不可变对象?
  5. A.txt 与 B.txt 两个路径同字节内容,为何既可以保留两个 Tree 条目,又可以只有一个逻辑 Blob?
  6. Main.java 版本1→版本2,为什么直接父 Tree 与根 Tree 通常都要形成新对象,而未变化的 docs Tree 可复用?
  7. 把 docs/B.txt 重命名为 manuals/B.txt,内部子 Tree/Blob 可以保持相同 ID,哪个父 Tree 必然要变化?为什么这与更换 Windows 工作树根目录名不同?
  8. 为什么 empty-folder/docs2/ 不因两层目录存在就自动进入普通提交,放入 empty-folder/docs2/.gitkeep 后情况怎样变化?此事是否说明 Git 禁止空 Tree?
  9. HEAD=C1、Index=Version2、工作树=Version3 时,git status 能看到哪两类变化?git switch 遇到可能覆盖未提交修改时能否无条件切换?
相关推荐
2301_7671139815 小时前
点过“不再询问”就要检查:Claude Code权限文件也可能存着密钥
git·信息安全·claude code
miofly16 小时前
Genie 4新增原生音频,实时世界模型加入声音生成
开源·github
dong_junshuai17 小时前
每天一个开源项目#155 MuJoCo:从接触求解到控制时序的物理引擎
github
GitHub实测笔记17 小时前
eza 本机实测:一万文件 -l 中位 48ms、ls 21.5ms;脚本里裸跑 eza 输出为空
github
一条Go17 小时前
开源新生代雷达 · 第 04 期
github
一条Go17 小时前
告警指纹:一次故障为什么只产生一条 FIRING 记录
后端·架构·github
wyxspark17 小时前
在本地 AI 中实现绝对可复现:一份 12,810 条哈希的硬核工程记录
github
简创AIGC陶先生17 小时前
【直播切片工作台】第31章:任务接口与任务服务
github
江屿风1 天前
【Git-组件】【Git入门:从文档灾难到版本掌控】流食般投喂
运维·笔记·git·组件·版本控制