10.03 Git 状态与版本记录:保存和解释一次 IM 规则修改
本章命令、状态和输出均为按规则推导的教学预期,编写时未执行这些练习。学习时请在自己新建的个人练习目录逐步核对,记录实际结果。
本章怎样学
先修只有 01.01 中的文件、编辑器、保存、终端和当前目录。本章用一个纯文本文件记录 IM 消息规则,不需要先会 Go 函数、自动测试、网络或数据库。
问题是:今天把消息长度上限从 6 字节改成 12 字节,明天需要回答“为什么改、具体改了哪一行、当时还有哪些规则”。仅靠文件名 最终版、最终版2 和聊天记录,很难稳定回答这些问题。Git 帮你保存可定位的版本与变更说明;规则是否合理,仍需要需求和验证证据。
| 阅读层次 | 小节 | 学完留下什么证据 |
|---|---|---|
| 起步 | 一、版本与三个位置 | 能画出工作区、暂存区、提交的关系 |
| 起步 | 二、第一份提交 | 一个独立本地仓库和初始规则 |
| 应用 | 三、差异与暂存 | 能预测暂存后再次编辑会提交哪个版本 |
| 应用 | 四、历史与说明 | 三份可解释的提交及其业务变化 |
| 第二遍 | 五、分支与合并 | 能解释快进、分叉和冲突的差别 |
| 第二遍 | 六、恢复与撤销 | 能指出操作的来源、目标及会丢弃什么 |
| 第二遍 | 七、远端与同步 | 能区分本地提交、获取历史和推送 |
| 综合 | 八、独立练习 | 一页状态记录和一次 IM 规则变更说明 |
第一次按一至四节 → 第八节的基础题学习。基础四节只需要本地 Git,不需要注册网站账号或配置远端。能独立解释三个版本之后,再读五至七节。
练习边界: 所有业务数据均为虚构。使用一个尚不存在的 im-git-practice 目录,它的父目录应是你自行选择的、位于其他仓库之外的个人练习目录。本文中的命令不应在课程仓库或工作仓库里执行。
一、先理解版本,再记命令
1.1 同一个文件为什么需要多份确定的状态
假设 message-rules.txt 起初约定:文本非空,按 UTF-8 编码后的字节数计算长度,上限为 6 字节。随后发生两个独立需求:
- 为了容纳一段更长的问候,将上限提高到 12 字节。
- 明确空格按原始输入参与计数,不自动去除首尾空格。
第一项改变了长度边界,第二项澄清了输入处理规则。如果两项修改混在一起,没有说明,就很难知道某个差异解决的是哪个问题。
一个版本是在某个位置可明确取出的文件状态。一次提交(commit)给被记录的项目状态一个身份,并附上说明等信息。保存文件只更新磁盘上的当前文件,不会自动创建提交。你可以多次保存、检查,再有意地提交一个可解释的阶段。
Git 可以记录纯文本、程序、配置和文档。Git 是版本管理工具;仓库是它管理的对象与引用等历史数据;GitHub 是可以托管仓库的服务。安装 Git 后,在本地创建提交并不需要 GitHub 账号。
1.2 工作区、暂存区、提交:三个位置各自回答一个问题
| 位置 | 你可以怎样理解 | 它回答的问题 |
|---|---|---|
| 工作区,working tree | 当前目录里可用编辑器读取、修改的文件 | 我现在正在写什么? |
| 暂存区,index,也叫索引 | 下一次普通提交准备使用的路径与内容状态 | 我这次打算记录什么? |
| 当前提交,后文记作 H | 当前历史位置保存的快照 | 上一次确定记录了什么? |
为了在纸上推演,分别用 W、I、H 表示这三个位置。它们是本章的记号,不是让你创建三个目录。
编辑器保存 git add git commit
↓ ↓ ↓
工作区 W ──选取当前内容──→ 暂存区 I ──创建快照──→ 新提交 H文件不会因为 add 从工作目录消失;提交后它也还在那里。可以把同一路径理解为同时有“已记录版本”“准备记录版本”“继续编辑版本”。三个位置的内容可以相同,也可以不同。
假设三个位置最初都写着“上限 6 字节”:
| 动作之后 | W:工作区 | I:暂存区 | H:当前提交 |
|---|---|---|---|
| 起点 | 上限 6 | 上限 6 | 上限 6 |
| 保存修改 | 上限 12 | 上限 6 | 上限 6 |
| 将文件暂存 | 上限 12 | 上限 12 | 上限 6 |
| 创建提交 | 上限 12 | 上限 12 | 上限 12 |
这个表比“Git 有哪些命令”更有用:遇到问题时,先问哪个位置的内容不符合预期,再决定命令。
1.3 提交保存快照,差异用于解释变化
一次提交在逻辑上指向一棵被记录的文件树,还记录父提交、作者与提交者信息、说明等。这里的“快照”不等于 Git 每次把整个目录复制一遍;内容不变时可以复用已有对象,存储还可能压缩。Pro Git 的模型说明
跟踪表示一份路径已进入 Git 的管理范围。新文件执行 add 后就已进入索引,虽然尚无首次提交,也已经在被跟踪;因此不能把“已跟踪”定义成“必须提交过”。后续的修改、删除同样需要被选择进下一份记录。Pro Git:记录修改
当前目录里的所有文件也不会自动进入快照。新文件需要纳入跟踪;已跟踪但没有再次暂存的修改,不会被本章使用的普通 git commit -m ... 自动带入。
差异是两个状态的比较结果。它适合回答“哪些行变化了”,而一个快照适合回答“这个版本的文件完整内容是什么”。后面分别用 diff 和 show 观察它们。
停下来预测: 文件已经保存,电脑重启后还能打开它,能否据此判断 Git 已经记录这个版本?不能。磁盘保存与 Git 提交是两个动作。Git 不会监视每次按下保存键并自动创建历史。
二、建立独立练习仓库,保存第一份规则
2.1 确认工具和当前目录
本章以 Git 2.28 或更高版本为命令基础:switch、restore 从 2.23 起提供,本章还使用 2.28 起提供的 init -b。在终端查看工具版本:
git --version预期会显示 Git 版本号,例如形如 git version 2.x.y;它不是要求你逐字匹配的固定输出。如果提示找不到命令,先按官方安装入口完成适合自己系统的安装,再继续。
下面的 mkdir、cd、pwd 目录命令适用于 Bash、Git Bash 或 PowerShell。若使用 Windows cmd,用不带参数的 cd 查看当前目录,替换这里的 pwd;后续 Git 命令仍按示例理解。
先用文件管理器或 cd 进入你选好的个人练习父目录,确认它位于任何已有仓库之外。在这个父目录中执行:
mkdir im-git-practice
cd im-git-practice
pwdmkdir 创建目录,cd 切换当前目录,pwd 显示当前位置。预期最后一段路径是 im-git-practice。如果该目录已经存在,先换一个新的练习名称,不要把已有内容直接当成本文的空白起点。
接下来除非明确另说,所有命令的工作目录都是这个 im-git-practice。
2.2 初始化与本地作者配置
git init -b main
git config --local user.name "IM Learner"
git config --local user.email "learner@example.invalid"逐条理解:
git init为当前目录建立 Git 仓库;-b main明确初始分支名称是main。git config设置配置;--local将配置限定在当前仓库,避免影响其他项目。user.name、user.email是提交中记录的作者身份字段。本例是练习身份,不需要填写公司信息。- 双引号让终端把带空格的名字作为一个参数。
这些身份字段不是网站登录凭据,也不授予服务器写权限。署名记录和身份认证是不同的事情。
通常会出现隐藏的 .git 目录,里面保存对象、引用、索引和配置等 Git 数据。业务文件放在练习目录中;不要用编辑业务文件的方式修改 .git 的内部文件。
刚初始化时,还没有第一份提交。此时 HEAD 指向预定的 main 分支名称,但尚没有一个提交可供取出。这叫尚未产生首个提交的分支状态。先完成下面的首次提交,再尝试后文依赖 HEAD 的历史或恢复命令。
2.3 用编辑器创建纯文本规则
在 im-git-practice 内用编辑器新建 message-rules.txt,保存为普通 UTF-8 文本,内容是:
消息类型:文本
空文本:拒绝
长度单位:UTF-8 字节
最大长度:6我们把这个内容称为 V1。V1 是课程给内容起的名字,不是 Git 自动生成的版本号。
这里的“字节”暂时只需理解为编码后数据的计量单位。后续语言章节会细讲。用于本题的确定事实是:ASCII 字母 a 占 1 字节,普通空格占 1 字节,你好 这两个 UTF-8 汉字合计占 6 字节。文件中的“最大长度”约束消息正文,不是限制这个规则文件自身的大小。
查看状态:
git status --short预期关键一行:
?? message-rules.txt?? 表示未跟踪:Git 看见了这个路径,但暂存区和现有提交中还没有这份文件。普通 git diff 不会把未跟踪文件的全部内容自动列出来;此时用编辑器阅读内容,用 status 确认文件身份。
2.4 暂存、检查、提交
git add -- message-rules.txt
git status --short
git diff --cached -- message-rules.txtadd 把执行那一刻的文件内容纳入暂存区。-- 表示后面的参数是路径,用于区分选项、版本名和文件名。这里每次明确指出文件,是为了让本次提交范围容易观察。
状态的关键一行预期变为:
A message-rules.txtA 表示相对于已有历史准备新增。--cached 表示查看暂存区相对于当前提交的差异;初次提交前没有 HEAD 提交,省略比较提交的 git diff --cached 会显示全部已暂存的新增内容。git diff 文档
核对四条规则后创建第一份提交:
git commit -m "docs: record initial IM text rules"
git statuscommit 创建提交,-m 后面是这份提交的说明。docs: 是我们采用的说明风格,提示这是文档修改;Git 不强制这个前缀。
预期出现一次提交记录,此后状态提示工作区干净。具体提示语言、统计数字和提交标识由环境决定。我们把这次提交称为 C1:
| 时点 | W | I | 当前提交 |
|---|---|---|---|
| 新建文件 | V1 | 没有该路径 | 尚无提交 |
add 后 | V1 | V1 | 尚无提交 |
commit 后 | V1 | V1 | C1,保存 V1 |
本节自查: “工作区干净”只说明 Git 当前报告的文件状态没有待提交变化。它不等于程序已经测试、服务已经发布,也不说明远端拥有这份提交。我们现在甚至没有配置远端。
三、暂存后再次编辑:为什么会同时有两种差异
3.1 先修改,再看尚未暂存的差异
前置状态:完成 C1,三个位置都保存 V1,工作目录仍是 im-git-practice。
用编辑器把最后一行改成:
最大长度:12保存后,这份内容称为 V2。观察:
git status --short
git diff -- message-rules.txt状态预期为:
M message-rules.txt行首第一个状态位置为空,第二个为 M。为了避免空格看漏,可以在纸上写成 [空格][M]。它表示暂存区与当前提交相同,工作区相对暂存区有修改。
差异的核心内容预期如下,前后元信息可不同:
@@ -1,4 +1,4 @@
消息类型:文本
空文本:拒绝
长度单位:UTF-8 字节
-最大长度:6
+最大长度:12- 开头表示旧侧存在而新侧移除的行,+ 开头表示新侧新增的行,开头为空格的是上下文。@@ 后面给出旧侧、新侧这一块涉及的起始行和行数。a/、b/ 若出现在完整输出里,是比较两侧的路径标签,不是要求你创建这两个目录。
显示为一行删除、一行新增,业务含义是上限从 6 提高到 12。Git 看见文本变化,不会自动判断这个上限是否满足用户需求。
3.2 暂存一个版本,再继续编辑另一个版本
先把 V2 放入暂存区:
git add -- message-rules.txt然后用编辑器在文件末尾追加并保存:
空格处理:保留原始空格,按字节计入长度完整工作区文件现在称为 V3:保留 V2 的上限 12,又多了空格约定。此时先不要再次执行 add。下面的 HEAD 在命令中用来选定当前提交,第四节会讲它与分支的关系。观察三种比较:
git status --short
git diff -- message-rules.txt
git diff --cached -- message-rules.txt
git diff HEAD -- message-rules.txt预期状态:
MM message-rules.txt在本节这种没有合并冲突的普通状态中,短格式的两个位置是 XY:X 比较暂存区与 HEAD,Y 比较工作区与暂存区。因此它同时报告两份差异。git status 文档
| 命令 | 比较哪两侧 | 当前应看到什么 |
|---|---|---|
git diff -- message-rules.txt | I → W | 增加空格处理行 |
git diff --cached -- message-rules.txt | H → I | 上限由 6 变 12 |
git diff HEAD -- message-rules.txt | H → W | 上限变化与空格行两项 |
这三个命令都不会因为查看差异就创建提交。最后一个也不把未跟踪文件自动包括进来。
3.3 现在提交,到底保存什么
保持上面的状态,执行:
git commit -m "docs: raise IM text limit from 6 to 12 bytes"
git status --short这次提交称为 C2。它保存 V2;工作区里的空格处理行仍在,但还没有进入提交。因此状态预期又是 [空格][M]。
完整轨迹如下:
| 动作之后 | W | I | H | 短状态 |
|---|---|---|---|---|
| C1 起点 | V1 | V1 | V1 | 无变化 |
| 改成上限 12 | V2 | V1 | V1 | [空格][M] |
add V2 | V2 | V2 | V1 | [M][空格] |
| 追加空格行 | V3 | V2 | V1 | [M][M] |
| 创建 C2 | V3 | V2 | V2 | [空格][M] |
所以 git add 选取的是当前内容,后来的保存不会自动更新暂存区。git add 文档
如果你真正想把 V3 一起提交,需要在提交前再次 git add -- message-rules.txt,然后重新检查 git diff --cached。本题故意将两项需求分开记录,因此现在先保留 C2,再做下一次提交。
3.4 为什么保留这个中间位置
设想你改了消息长度规则,同时顺手写了一段个人笔记。暂存区允许你明确组成一次提交:哪个文件、哪个版本应该共同出现。它不判断选择是否正确,所以检查暂存差异是提交前的一步必要思考。
本章用整个文件演示,后续 10.06 会讨论按差异片段组织提交。起步时先保持一次修改有一个清楚目的,就能减少“提交说明说改上限,实际还偷偷改了空格规则”的理解成本。
练习: 如果在 MM 状态下只保存文件、不再次 add,随后提交,空格行会消失吗?不会。它保留在工作区,成为尚未暂存的修改。没有进入这次提交与文件被删除是两件事。
四、阅读历史,解释一次业务变化
4.1 完成第三份提交
前置:C2 已完成,空格处理行只在工作区,当前目录仍为 im-git-practice。
git diff -- message-rules.txt
git add -- message-rules.txt
git diff --cached -- message-rules.txt
git commit -m "docs: clarify that spaces count toward the IM byte limit"
git status --short这次是 C3,保存 V3。预期短状态没有输出,三个位置都与 V3 一致。至此基础练习已有三个有明确目的的提交,不需要 Go 编译或在线 IM 服务。
4.2 HEAD、分支和提交身份
分支是一个可移动的提交引用,例如 main。普通附着分支的工作状态下,HEAD 指向当前分支,分支指向它的最新提交。
HEAD → main → C3
│ 父提交
↓
C2
│ 父提交
↓
C1C1、C2、C3 是课程图里的名字。Git 实际使用对象标识,你会在日志里看到它的缩写;不要把图中的 C2 原样当成命令参数。标识不是按“1、2、3”递增的版本号。显示缩写长度也不必在每个环境中完全一致。
提交保存了指向父提交的关系。图中向下表示沿父关系回到过去,不是文件复制方向。普通提交通常有一个父提交,初始提交没有父提交,后文的合并提交可以有多个父提交。
后续阅读固定版本时还可能看到 HEAD 直接指向提交,这叫分离 HEAD。现在先使用 main 上的普通状态;不要把“HEAD 永远等于 main”当成通用规则。
4.3 用几个问题选择查看命令
在练习仓库根目录执行以下只读命令:
git log --oneline --decorate --graph
git log -3
git show HEAD -- message-rules.txt
git show HEAD:message-rules.txt
git diff HEAD~1 HEAD -- message-rules.txt| 命令 | 问题与预期 |
|---|---|
log --oneline --decorate --graph | 历史怎么连接,哪些引用指向这里?显示简写提交、说明、关系线和引用标注 |
log -3 | 最近三个提交记录了什么作者、时间和说明? |
show HEAD -- 文件 | 当前提交对这个文件引入了什么变化?在当前普通提交示例中应看到空格行 |
show HEAD:文件 | 当前提交中这份文件的完整内容是什么?应是 V3 |
diff HEAD~1 HEAD -- 文件 | 当前提交与它的第一父提交相比变化了什么?此处比较 C2 与 C3 |
HEAD~1 表示沿第一父提交关系退一步。不是“昨天的版本”,也不是“减去一个哈希值”;在 C1 上使用它会失败,因为 C1 没有父提交。
HEAD:message-rules.txt 中的冒号用来选择提交中的文件。它是查看内容,不会把工作区文件替换掉。有些终端将长输出放到分页器中,看到分页界面时通常可以按 q 退出查看。
4.4 提交说明怎样帮助别人接手
下面两种说明的信息量不同:
updatedocs: raise IM text limit from 6 to 12 bytes后一种能直接回答改动对象、方向与单位。更完整的变更说明还应写出:动机是容纳四个本题使用的普通汉字;空文本依然拒绝;长度仍按 UTF-8 字节计算;需要检查 6、12、13 字节边界。不要声称“所有语言都是一个字三个字节”,这里给定的中文例子不构成通用字符规则。
可以在个人学习记录里用下面的结构:
| 项目 | 这次改上限的说明 |
|---|---|
| 原问题 | 你好你好 有 12 字节,在上限 6 时被拒绝 |
| 变化 | 上限从 6 改为 12,恰好等于上限仍允许 |
| 保留规则 | 空文本拒绝,按 UTF-8 字节数计算 |
| 证据 | 给出预测边界表;实际验证情况单独记录 |
| 尚未覆盖 | 不涉及身份认证、真实发送、持久化或对端接收 |
提交身份只定位这份记录。它不证明内容经过评审,不证明作者字段不可伪造,也不证明消息已送达。Git 的证据是“这份内容和记录是什么”,业务保证需要另外建立。
五、第二遍阅读:分支、快进与合并冲突
5.1 分支让两个方向有各自的位置
现在已经知道提交与父关系,再看分支会容易得多。分支不是复制一套项目历史的文件夹;创建分支首先是增加一个指向提交的名字,之后它可以随新提交向前移动。
本节实验接在 C3 后,要求工作区干净。先确认:
git status --short
git switch -c clarify-boundaryswitch 切换当前分支,-c 表示同时新建。本例新名字与 main 起初都指向 C3,HEAD 改为指向 clarify-boundary。
用编辑器在文件末尾增加一条具体边界示例:
边界示例:你好你好占12字节,符合本规则的长度上限随后:
git add -- message-rules.txt
git diff --cached -- message-rules.txt
git commit -m "docs: add a concrete 12-byte IM boundary example"
git log --oneline --decorate --graph --all将新提交记为 C4。这时关系是:
C1 ← C2 ← C3 ← C4
↑ ↑
main clarify-boundary ← HEAD--all 让日志包含所有引用可到达的历史。在 clarify-boundary 提交使这个分支向前移动;main 仍停在 C3。
5.2 快进为何不需要额外的合并提交
前置:C4 已提交,工作区干净,没有其他分支修改混入。仍在同一练习目录:
git switch main
git merge --ff-only clarify-boundary切回 main 后,跟踪文件恢复为 C3 的内容,边界示例行暂时不在当前版本里。merge 请求把指定分支的历史整合到当前分支;--ff-only 要求只能快进。
C3 本来就是 C4 的祖先,main 可以直接从 C3 移到 C4,这就叫快进。此时工作区更新为 C4,两个分支指向同一提交,不需要新造一个“合并提交”。
C1 ← C2 ← C3 ← C4
↑
main ← HEAD
↑
clarify-boundary本课切换前先保持干净,是为了让状态容易推演。实际 git switch 并不总要求干净:可以保留不冲突的未提交修改;可能覆盖本地修改时通常会拒绝。不能把切换分支当成自动保存或自动清空工作区的办法。git switch 文档
5.3 分叉是历史关系,冲突是整合问题
下面是另一个独立情景,不接着执行上一实验。以共同版本 C1 为起点,一个分支把上限 6 改成 12,另一个把同一行改成 24:
C2(上限 12,main)
/
C1(上限 6)
\
D2(上限 24,larger-limit)这叫分叉:两个分支各有对方没有的提交。--ff-only 此时拒绝整合,因为移动一个名字不能同时表达两条变化。
普通合并会结合共同基准与两边变化。两个分支改不同位置可能自动合并;两个分支对同一行做不兼容的改动,可能产生冲突。因此“历史分叉”不等于“必然有文本冲突”。
若当前在 main,正在合并 larger-limit,一种常见冲突标记形如:
<<<<<<< HEAD
最大长度:12
=======
最大长度:24
>>>>>>> larger-limit上半段来自当前一侧,下半段来自被合入一侧;标记标签与格式也可能随配置变化。它们不是规则文件应保留的正文。
5.4 解决冲突需要业务决定
在上面的独立冲突情景里,先确认目前有一次进行中的合并,再按顺序处理:
- 用
git status找到冲突路径,查看git diff和文件内容。 - 查清需求到底批准 12 还是 24,或者需要另一条有前提的规则。不能仅因 24 更大就选择它。
- 用编辑器写出最终文件,删除冲突标记,核对其他规则仍成立。
git add -- message-rules.txt把解决后的内容放入索引,表示这份路径已处理。- 再查看暂存差异,使用
git commit -m "docs: reconcile the agreed IM text limit"完成合并提交。
add 不会替你证明冲突解决正确。它记录你选择的结果,业务判断仍由你负责。普通 XY 状态解释也不能直接套在未合并路径上,冲突有专门的状态含义。
如果决定放弃这次正在进行的合并,可用 git merge --abort 尝试回到合并前。这个说明以开始前干净为前提;若合并前已有未提交内容,尤其合并中又修改它们,不能保证恢复到每一份原始内容。git merge 文档
完整的合并、rebase 和团队评审流程在 10.06 深化。本节先建立模型,不要求第一次学习就人为制造冲突。
六、第二遍阅读:恢复文件与撤销提交是不同的操作
6.1 先写清楚“从哪里恢复到哪里”
“改错了,帮我回退”还不够明确。可能是取消本次暂存,可能是覆盖尚未提交的编辑,也可能是保留历史并撤销某个已提交行为。先回答:
- 希望哪个位置改变:工作区、暂存区,还是分支指向?
- 目标内容在哪:暂存区、某个提交,还是只有编辑器里?
- 现在的修改是否还要保留?目标提交是否已经被别人使用?
下面的恢复表是独立推演题。每一行都从相同起点开始,不要从上到下连续执行:已有提交 H 的上限为 6,索引 I 为 12,工作区 W 为 24。它们均是同一份已跟踪文件。
| 操作 | 读取来源 | 修改位置 | 操作后的 H / I / W |
|---|---|---|---|
git restore --staged -- message-rules.txt | H | I | 6 / 6 / 24 |
git restore -- message-rules.txt | I | W | 6 / 12 / 12 |
git restore --source=HEAD -- message-rules.txt | H | W | 6 / 12 / 6 |
取消暂存不等于丢弃编辑。默认恢复工作区也不等于一律回到最后提交:没有 --staged 时默认来源是索引;指定 --staged 且未显式选来源时,默认来源是 HEAD。git restore 文档
第三行执行后索引依旧为 12,因此此时直接普通提交仍会提交 12。工作区与索引甚至可以朝不同方向变化。用状态模型能解释这种结果,仅记“restore 是撤销”容易误判。
6.2 覆盖前确认修改是否还要用
restore 是写入目标位置的操作。恢复工作区前应先用 diff 阅读即将被覆盖的修改;需要保留的内容可先另存清楚命名的副本,或在理解的前提下记录成提交。不要为了让状态变干净而丢弃不明来源的编辑。
如果文件从未暂存、从未提交,Git 历史中就可能根本没有它的内容。编辑器的恢复记录与文件备份属于其他机制,不能把希望寄托在“Git 应该记得我写过”。
对于刚初始化、尚无第一次提交的仓库,依赖默认 HEAD 来源的恢复命令不满足本节前提。先完成第二节的初始提交,再练习这里的表格。
6.3 reset:移动历史位置与重置内容
这里讨论带目标提交、没有路径参数的 reset 形式;带路径的 git reset -- 文件 是另一种索引操作,不能把所有 reset 都概括为移动分支。
独立情景:在自己的本地普通分支上有 C1 → C2,当前 C2 的快照是 V2,W、I 都干净且是 V2。你确认 C2 尚未分享,希望重新组织它。
| 概念命令,各行独立 | 分支位置 | 索引 | 工作区 |
|---|---|---|---|
git reset --soft HEAD~1 | 移到 C1 | 保留 V2 | 保留 V2 |
git reset --mixed HEAD~1 | 移到 C1 | 改成 V1 | 保留 V2 |
git reset --hard HEAD~1 | 移到 C1 | 改成 V1 | 跟踪内容重置为 V1 |
--mixed 是这类调用的默认模式。--hard 会覆盖工作区修改,还可能覆盖或移除阻挡目标路径写入的未跟踪内容,不能理解成“只动已经提交的文件,其他都安全”。这张表用于辨认语义,不把 --hard 作为起步练习的常规操作。git reset 文档
移动分支不代表旧提交对象立刻消失,也不代表它永久有保障。不要因此忽略未提交内容可能被覆盖的事实。
6.4 revert:用新记录撤销已记录的变化
如果 C2 已经分享,其他人以它为基础工作,把自己的分支向回移动会造成历史不一致。一个常见做法是保留 C2,再增加一份明确撤销其效果的提交。
另取一个干净的独立情景:只有 C1、C2 两个普通单父提交;C1 上限 6,C2 唯一变化是改为 12,当前位于 C2。先检查它:
git status
git show HEAD -- message-rules.txt确认目标和前提后,下面命令会实际创建新提交:
git revert --no-edit HEAD--no-edit 表示采用默认生成的撤销说明。预期新增 R3,其规则内容回到上限 6,C2 仍在历史里:
C1(6) ← C2(12) ← R3(6) ← main ← HEADR3 与 C1 的这份文件内容可以相同,但提交身份、父关系和说明不同。后来的人可以看见“曾扩大上限,后来决定撤销”。git revert 文档
撤销较早提交时,后续变化可能与反向修改冲突,需要重新判断。不能保证 revert 总是无冲突,也不能把它理解为“把全部目录精确搬回某个旧快照”。合并提交的撤销还有父关系选择,留到 10.06。
即使规则文件撤销成功,真实系统中已经发出的消息、写入的数据库记录或发布的服务都不会因此自动回滚。Git 操作改变代码与文档历史;运行状态的恢复是后续交付课程的问题。
6.5 reflog 是线索,不能替代备份
后续可能看到 git reflog。它记录本地引用近期怎样移动,在某些误操作后能帮助找到旧提交。它不是每次编辑文件的录像,记录与对象也有保留、清理条件。
因此不能承诺找回从未记录的文件、所有未暂存编辑或被外部程序覆盖的内容。对于重要内容,清楚的提交与合适的独立备份仍有价值。
本节判断题: 想让一行暂时不进入下次提交、但继续留在编辑器中,应选择取消暂存;想撤销团队已使用的提交,要讨论保留共享历史的反向提交。两者目的不同,不能用同一个“回退”词省略判断。
七、第二遍阅读:远端记录在哪里,怎样同步
7.1 三个“main”可能处在不同位置
远端仓库是另一份仓库,常托管在服务器上,也可以位于另一个本地路径。origin 通常只是你给一个远端地址取的别名;不是 Git 中唯一合法的名字,也不是网站本身。
理解以下三个位置:
| 名字 | 存放位置 | 含义 |
|---|---|---|
本地 main | 你的仓库 | 你正在维护的本地分支 |
本地 origin/main | 你的仓库 | 按配置获取后,本地记录的远端分支位置 |
服务器的 main | 远端仓库 | 服务器实际保存的分支位置 |
第二行叫远端跟踪引用。它不是每秒刷新一次的服务器画面。别人推送后,服务器可能已经前进,本地 origin/main 暂时还停在旧提交。
你的本地仓库 服务器仓库
main → C2 main → C3
origin/main → C2网络同步要明确更新的是哪一个位置。只看见本地 origin/main 与 main 相同,不能据此证明此刻服务器没有新提交。
7.2 clone、fetch、集成和 push 各自做什么
以下是概念表,不要求现在为练习创建远端。若将来有一个自己有权限、已经配置为 origin 的练习远端,才在其本地仓库根目录使用对应命令。
| 动作 | 常见命令 | 主要影响 |
|---|---|---|
| 查看配置 | git remote -v | 显示已配置远端的地址 |
| 克隆 | git clone 地址 | 新建本地仓库,获取历史并建立初始引用和工作区 |
| 获取 | git fetch origin | 获取对象,按配置更新远端跟踪引用等记录 |
| 查看分叉 | git log --oneline --graph --decorate --all | 观察本地已知历史关系 |
| 比较内容 | git diff main origin/main | 比较两个本地引用所指快照 |
| 单独集成 | git merge --ff-only origin/main | 在可快进时更新当前分支和工作区 |
| 获取后集成 | git pull --ff-only origin main | 获取指定分支,按只允许快进的要求整合 |
| 推送 | git push origin main | 请求服务器接收对象并更新目标分支 |
其中单独集成与 pull 是两种安排,不必机械地每次都连续执行。表中的 merge 前应确认当前分支是 main,并保持本课要求的干净状态。
clone 通常为默认分支建立一个本地分支,再记录其他远端分支;它不会自动把每个远端分支都变成当前活动分支。克隆入口的地址是要按实际公开项目或个人练习仓库填写的参数,不要把占位词“地址”原样执行。
7.3 fetch 后,为何文件可能完全没变
承接 7.1:本地 main、origin/main 在 C2,服务器 main 在 C3。普通配置下获取后:
你的本地仓库 服务器仓库
main → C2 main → C3
origin/main → C3此时你拥有了新历史和更新后的跟踪位置,但当前 main 及其工作区尚未合入 C3。先看图和差异,再决定怎样整合,就是将“获取信息”与“改变当前工作状态”分开理解。git fetch 文档
pull 把获取和后续整合放在一个入口里。整合方式受参数、版本和配置影响,可能是快进、merge、rebase 等;不能永远简写成“pull 就是 fetch 加一个固定的 merge”。本课显式写 --ff-only,遇到分叉会拒绝快进,让你先理解历史。git pull 文档
7.4 push 不会把编辑器里的未提交修改上传
假设本地 main 指向 C4,工作区又多写了一条尚未提交的规则。git push origin main 请求同步的是由 main 可达的已提交对象与引用;那条只在工作区里的规则不在这个分支历史中,不会自动被推送。
推送也不等于部署。托管平台可能另有流水线,但是否触发、是否成功、发布到了哪,需要各自的证据。git push 文档
如果服务器已经有你尚未整合的提交,推送可能以非快进为由拒绝。应先获取、观察分叉并确认协作方案;“强制覆盖让报错消失”不是这个问题的默认答案。
无需把真实凭据写入教材或学习记录。user.email 的署名配置也不能替代远端要求的身份认证与仓库授权。
八、综合练习:让版本记录能解释一次 IM 需求变化
8.1 第一轮交付:三个提交与一张需求表
完成第二至四节后,应留下 C1、C2、C3 三份可解释记录,以及下表的预测。这里按给定 UTF-8 内容计算,不要求现在编写程序:
| 消息输入 | 本题字节数 | C1:上限 6 | C2:上限 12 | C3:上限 12,明确原样保留 |
|---|---|---|---|---|
| 空文本 | 0 | 拒绝 | 拒绝 | 拒绝 |
a | 1 | 允许 | 允许 | 允许 |
你好 | 6 | 允许 | 允许 | 允许 |
你好a | 7 | 拒绝 | 允许 | 允许 |
你好你好 | 12 | 拒绝 | 允许 | 允许 |
你好你好a | 13 | 拒绝 | 拒绝 | 拒绝 |
| 一个普通空格 | 1 | 需确认清理约定 | 需确认清理约定 | 允许 |
你好,两侧各一个空格 | 8 | 需确认清理约定 | 清理与否均未超限,处理方式待确认 | 允许,原样保留 |
C2 只改上限,没有替未说明的空格策略作决定;C3 将策略写清。V3 是对原先留白约定的澄清,不声称一个已经存在的程序必然改变行为;若原实现本来就保留空格,改变的是文档的明确程度。因此不能事后把 C3 的约定假装成每一份旧文档本来都有的承诺。需求表中的“允许”仅指符合本文本规则,不代表消息被网络接收或对端读到。
学习产物建议包含:三个实际提交标识、每次暂存前后的状态表、一个你预测错误后修正的解释,以及实际命令结果。教材中的预期不能代替你的执行记录。
8.2 不想记录的个人笔记怎样处理
假设你在仓库内新建 notes.local.txt,只记录个人学习进度,不想纳入版本。可以用编辑器创建 .gitignore,添加:
notes.local.txt.gitignore 是一份忽略规则文件。匹配的未跟踪文件通常不会出现在普通状态提示中,也不会被普通批量添加纳入。但 .gitignore 本身可以作为项目约定被跟踪。
如果 notes.local.txt 早已被跟踪,后来添加忽略规则不会自动取消跟踪,也不会从已有提交中抹去内容。gitignore 文档
这个例子在完成前三次提交后独立尝试;若要记录 .gitignore,它会产生另外一次有目的的提交。不要为了让提交次数符合教材就合并无关内容。更不能把忽略规则当作已提交敏感数据的清除方案。
8.3 分层练习与反馈
先自己写出答案,再展开反馈。第 1–8 题适合第一次学习,后面结合第二遍章节。
练习 1:保存与提交。只用编辑器把上限 6 改成 12,再保存,Git 历史里已有 12 吗?
反馈: 没有自动产生。工作区是 12,暂存区与当前提交仍是 6。先看状态和差异,再有意暂存、提交,才建立新历史。
练习 2:三个位置。H=V1、I=V2、W=V3,现在普通提交会记录哪份?
反馈: 记录 V2,形成新的当前提交;工作区仍为 V3。这是本章使用的无路径、无 -a 的普通提交形式。不要用另一个命令变体的行为替代当前前提。
练习 3:同一状态下,git diff 与 git diff --cached 分别能回答什么?
反馈: 前者比较 I 与 W,即 V2→V3;后者比较 H 与 I,即 V1→V2。先写出两侧,再解释差异,避免凭“cached 看起来像缓存”猜含义。
练习 4:为什么首次提交前能用 git diff --cached,却不能假设 git show HEAD 可用?
反馈: 还没有提交对象可供 HEAD 解析。省略比较提交的暂存差异命令支持此情况,会显示全部已暂存内容;查看 HEAD 快照则需要那个提交已经存在。
练习 5:新建了一份规则文件,git diff 没有显示它,能否认定没有修改?
反馈: 不能。它可能尚未跟踪。结合 git status 查看 ?? 路径,再阅读文件、决定是否加入暂存区。只观察一种 diff 并不能覆盖所有工作状态。
练习 6:为什么 Git 用户名写成某个管理员,并不会获得项目写权限?
反馈: 署名字段描述提交的作者信息;远端身份认证和权限检查由另外的机制完成。身份字段可填写不等于该身份被可信验证。
练习 7:上限由 6 改成 12,给出一个行为改变和一个行为保留的输入,并写提交说明。
反馈: 你好a 为 7 字节,由拒绝改为允许;空文本仍拒绝,或 你好你好a 为 13 字节仍拒绝。说明要写清上限、单位和原因,不能只写“优化规则”。这些是根据需求推导的预期,未执行时不要写测试已通过。
练习 8:工作区干净,是否说明记录已同步给别人?
反馈: 不说明。它反映本地文件状态。本地提交可能从未推送,甚至仓库完全没有远端。还要区分同步、评审、测试与部署的证据。
练习 9:H 的上限 6、I 为 12、W 为 24。想保留 24,只取消暂存,结果应是什么?
反馈: 使用本节前提下的 git restore --staged -- message-rules.txt,得到 H=6、I=6、W=24。若误用没有 --staged 的默认 restore,工作区会被索引里的 12 覆盖,目标就错了。
练习 10:已经分享的扩大上限提交需要撤销。为何通常采用新增反向提交?
反馈: 让共同使用的原历史继续存在,并明确记录撤销理由和结果。还要检查后续依赖、冲突和部署状态;不能说一次 revert 自动撤销所有线上效果。
练习 11:两个分支改不同文件,历史已分叉。是否仍能使用 --ff-only?是否必然有冲突?
反馈: 已分叉,无法仅移动当前分支完成快进,因此 --ff-only 拒绝。不同文件的修改仍可能普通合并成功;分叉和文本冲突不是同一个判定。
练习 12:fetch 后 origin/main 前进,main 不动,这算获取失败吗?
反馈: 不一定。普通配置下这符合获取语义:更新本地掌握的远端历史,尚未集成到当前分支。查看图、差异及命令结果后,再决定整合方式。
练习 13:文件已经提交,后来加入 .gitignore,它会从历史消失吗?
反馈: 不会。忽略规则主要作用于未跟踪路径的识别与普通添加,不会自动移除已跟踪文件,也不重写历史。文件内容是否应保留需要单独处理。
练习 14:冲突一侧是上限 12,另一侧是 24,选择数字更大的就完成业务验证了吗?
反馈: 没有。先确认需求、资源限制和兼容约定,再选择并验证规则。删除冲突标记、add、commit 只是把决定记录下来,不代表这个决定必然正确。
练习 15:一份从未暂存或提交的笔记被覆盖,能否保证用 reflog 找回?
反馈: 不能。reflog 记录引用移动,不是所有文件编辑。需要看是否有编辑器记录、备份或其他实际保存机制;没有证据时不能许诺可恢复。
8.4 将同一个模型带到 OpenIM 阅读
课程固定参考 OpenIM v3.8.3-patch.16,版本地图记录其对应提交为:
f6411a8a1a31d3df36f4c2b3ad28481a94141e1f现在你能理解为什么要同时记录版本标签和提交身份:标签给人一个易读名称,提交标识用于定位具体历史状态。标签是一种引用,技术上也可能被重新指向;固定源码链接明确选定哪份内容,避免日后默认分支变化导致教材与代码不对应。
可以直接在这个固定提交的公开文件树阅读,不要求现在克隆、运行项目或理解全部 Go 代码。本章只使用公开版本定位这个事实,没有审计或虚构该提交相对于上一版本的业务变化。完整定位依据见课程的 IM 阅读地图。
这里先阅读发布记录和固定文件位置;实际源码差异需要同时选定起点与终点,不能只凭一个版本标题推断所有修改。下一步遇到一份真实源码差异时,仍先问:比较的是哪两个版本,哪些规则变化,为什么变化,哪些证据支持它。语法更复杂,版本与差异模型仍然适用。
8.5 本章验收与下一步
达到本章基础目标,需要你独立做到:
- 不看答案画出一次编辑、暂存、再次编辑、提交的 W/I/H 表。
- 找到三个提交,分别展示某一份完整文件和一次差异。
- 用一个正常输入和一个边界输入说明上限变化的业务效果。
- 区分“编辑已保存、版本已提交、远端已更新、业务已验证”。
第二遍再补:解释一次分叉,判断恢复命令的来源和目标,说明共享撤销与远端同步的边界。学习进度分别记录“能解释”“自己做过”“遇到变式还能判断”,不把阅读完成直接等同掌握。
回到主线,01.04 会把单条消息扩展为数组、切片与文本处理;10.01 深化规则、边界与验收的来源;10.06 代码审查与合并再把本章的分支与差异模型用于实际变更评审。你现在已经能保存学习过程中每一次可解释的变化。
参考资料
- Pro Git 2e:按模型、基本操作、分支与远端逐步复习。
- git commit:核对普通提交与其他形式的区别。
- git log、git show:阅读历史、对象及差异。
- gitrevisions、git reflog:核对版本表达式与引用日志的范围。
每节的命令规则还链接到对应官方手册。教材采用自己的虚构规则与状态表组织练习,具体输出文字可能随版本、语言和终端设置变化;判断重点是对象、引用及三个内容位置的关系。