2.4 暂存文件(git add)


2.4 暂存文件(git add)

本节摘要git add 是把工作区修改登记进暂存区的桥——它不复制文件,而是记录当前内容快照并更新索引。本节讲清 add 在"三区模型"中的位置、为什么需要暂存区,再逐个演示四种用法:添加单/多文件、添加目录、添加全部变化、交互式部分暂存(-p),最后用 status 验证效果。

本节导读

阅读完本节,你应当能够:

  1. 解释 add 为什么是"记录快照"而不是"移动文件"。
  2. 区分 git add 文件、git add 目录、git add -A 三者的覆盖范围。
  3. 用 git add -p 交互式地只暂存文件的一部分修改。
  4. 通过 git status 的输出来判断哪些修改已被暂存、哪些没有。

一、问题与直觉

假设你一个下午干了三件事:修了一个 bug(改了 app.js)、给文档加了一段说明(改了 readme.md)、顺手调了一下配置(改了 config.json)。现在你想提交,但不想把三件事混在一条历史里——你希望历史记录能讲清楚"这条提交是修 bug,那条是改文档"。

没有暂存区的系统怎么做?要么一次提交三件混着的事,要么为每件事单独开一个分支来回切。两种都笨。

Git 的解法就是在中间插一个暂存区:你先用 add 把 app.js 的改动放进暂存区,提交一次(提交信息写"修复 bug");再把 readme.md 放进去,提交一次("更新文档");最后处理 config.json。三次 add 夹两次 commit,历史清晰如水晶。

这就是 add 存在的全部意义:它让你决定"下一次提交装什么"。工作区是你的草稿纸,暂存区是你要装订的页签,commit 才是装订成册的动作。

二、核心原理

add 在模型中的位置

回顾三区模型:工作区是你编辑文件的地方,暂存区是"待提交清单",仓库是历史档案。add 就是从工作区往暂存区输送内容的管道。

一个关键细节再强调一遍:add 不是把文件"搬"过去,而是记录文件当前内容的快照,然后在索引(.git/index)里登记"这个文件的新版本要进下次提交"。所以:

  • add 之后你又改了同一个文件,暂存区里仍是 add 那一刻的版本,工作区的新改动要再 add 一次。
  • add 不影响工作区里的文件,你的文件保持原样。

这个"快照登记"机制,是 Git 相比"复制粘贴式版本管理"的核心优势——它记录的是"意图"(我选了哪些版本),而不是机械地复制文件。

add 的四种用法

在讲用法之前,先补一个底层机制:add 往对象库写入什么。当你执行 git add index.html 时,Git 会做三件事——把 index.html 的内容压缩成一个 Blob 对象写进 .git/objects;给这个对象算一个 SHA-1 哈希;把"index.html 应指向这个对象"的引用更新到索引文件。整个过程在本地完成,毫秒级,不涉及任何网络。

这意味着:add 已经占用了对象库空间(写入了一份内容快照),即使你后来不提交、用 restore --staged 撤下,这个对象也可能残留在对象库里(直到被垃圾回收清理)。对普通项目这无足轻重,但它解释了为什么说"add 是快照"——快照在 add 那一刻就生成了,commit 只是把它正式挂进历史。

图 2-4 add 时刻对象库里发生了什么

图 2-4 add 时刻对象库里发生了什么

一、添加指定文件。 最精确的用法,指定一个或多个文件:

git add index.html git add style.css script.js # 多个文件空格隔开

二、添加整个目录。 递归添加目录下所有新建和修改的文件:

git add src # src 目录下所有变化

三、添加全部变化。 一个点代表"当前目录全部新建和修改的文件":

git add .

注意:git add . 不包含已删除的文件。想连删除一起记录,用 -A(--all):

git add -A

-A 是"所有变化"(新建、修改、删除)的完整写法,等于 git add .(处理新增修改)加上 git add -u(处理已跟踪文件的修改与删除)。

四、交互式部分暂存。 最精细的用法,只暂存一个文件里的部分修改块(hunk):

git add -p index.html

执行后 Git 会逐个显示修改块,问你怎么处理,常用选项:

选项 含义
y 暂存当前块
n 不暂存当前块
q 退出
a 暂存本文件剩余所有块
s 把当前块拆成更小的块
e 手动编辑当前块

-p 是"把提交拆得跟你的思路一样细"的高级武器。它的典型场景:一个文件里同时改了功能代码和格式排版,你想把两件事拆成两次提交。

add 向左是登记,commit 向右是固化。add 决定"装什么",commit 决定"什么时候装"。

三、工程实践要点

用 status 验证 add 的效果

add 本身几乎不输出信息(除非出错),它是否成功要看 git status。理解 status 的三个分区:

status 分区 含义 对应动作
Changes to be committed 已暂存,准备提交 已 add,下一步 commit
Changes not staged for commit 已跟踪但工作区有新改动 未 add,需要 add
Untracked files 从未被 Git 跟踪的新文件 需要 add 才开始跟踪

一个经典场景:你 add 了 app.js,然后又改了它。此时 status 会同时显示:

  • "Changes to be committed" 里的 app.js(暂存区里的旧版本)
  • "Changes not staged for commit" 里的 app.js(工作区的新改动)

看到这个不要慌,它是"三区不一致"的正常呈现,意味着你需要决定:再 add 一次把新改动也纳入,还是不 add 只提交旧版本。

一个完整的 add 实操演示

跟着敲一遍,感受状态如何一步步变化:

echo a > a.txt echo b > b.txt git add a.txt b.txt # 两个文件都进暂存区 git status # 看到两个文件都在 "to be committed" echo c > c.txt # 新建第三个文件 git status # c.txt 出现在 Untracked files git add . # 把 c.txt 也加进来 echo a2 >> a.txt # 又改了 a.txt git status # a.txt 同时出现在两个分区 git add a.txt # 再次 add,同步最新版本 git commit -m "添加初始文件" # 全部固化

最后一次"a.txt 同时出现在两个分区"尤其值得体验——它是理解"add 是快照"的活教材。多跑几遍这个序列,直到你能预判每一步 status 的输出。

选哪种 add 用法的决策表

场景 推荐用法 理由
新项目首次提交 git add . 快速纳入全部
只提交部分文件的修改 git add 文件... 精确控制
一次提交所有变化含删除 git add -A 完整捕捉
一个文件里拆两类修改 git add -p 原子提交
日常小改动 git add -A 或 git add . 省事

关于 -p 的更深一层的价值

git add -p 看起来只是"高级技巧",但它背后是一种提交哲学:一次提交只做一件事(Single Responsibility)。真正成熟的仓库里,历史不是流水账,而是一部"为什么这么改"的编年史——每条提交都能被独立回滚、独立审查、独立 blame。想达到这个标准,-p 是日常武器。

实际操作中它的常见搭档是 git diff(看未暂存改动的具体内容)和 git diff --cached(看已暂存改动的具体内容)。先 diff 后 -p,你才能在拆块时知道每一块到底是什么。

一个常见的实操误区

很多新人习惯一上来就 git add .git add -A,把所有东西一股脑塞进暂存区。这没有错,但它抹掉了暂存区最大的价值——选择性。如果你本来就想"一次提交所有改动",那没问题;但如果你手里同时有"功能 A 改了一半"和"功能 B 已完成",全 add 之后再提交,两条线就缠在一起了。养成习惯:提交前先想"这次提交该包含什么",再决定 add 什么。哪怕只花十秒钟,历史质量会有质的差别。

⚠️ 常见坑:git add . 不包含已删除的文件。删了文件却只 add 了目录,提交后删除不会进历史,仓库里还是留着旧文件。

💡 关键直觉:把 add 想成"挑菜"——你从案板上挑出要下锅的菜登记到点菜单,没挑的留在案板。commit 是"起锅",只炒点菜单上的菜。挑菜的质量决定这一锅菜的水平。

常见问题与 FAQ

"git add 后撤销怎么办?"git restore --staged 文件 把文件从暂存区撤下(保留工作区修改),2.7 节会详细讲。这个操作完全可逆,add 错了不必慌。

"git add 能添加忽略的文件吗?" 不能,除非用 -f 强制。被 .gitignore 匹配的文件,Git 默认不理会。想确认为什么某个文件没被 add,用 git check-ignore 查一下。强制添加一般只在确认该文件必须入库、且忽略规则写错了时才用。

"add 和 commit 之间隔很久会不会有问题?" 不会。暂存区只是清单,内容在对象库里按需存储。但建议别让暂存区"过期太久"——放久了你会忘记当时为什么选这些版本,写提交信息时还得回想。理想状态是 add 完尽快 commit,让暂存区始终反映"即将发生的事"。

"git add -A 和 git add . 到底啥区别?" 区别在删除文件的处理:add . 只加新建和修改,add -A 连删除一起。日常图省事用 -A,想精确控制用明确路径。

add 的几个边界情况

空白提交问题:如果你 add 了文件但内容根本没变(比如 touch 了一下没改动),git add 不会产生任何暂存效果,commit 时会提示"没有要提交的改动"。这不叫 bug,叫 Git 在帮你把关——别提交无意义的空提交。

符号链接与二进制文件:add 对符号链接处理的是链接本身而非指向的内容;二进制文件(图片、模型)也能 add,但 diff 时 Git 只能判断"变了没变",看不出变化细节。大二进制文件频繁改动会让仓库迅速膨胀,这类资产通常不该进 Git 仓库,而是用专门的对象存储或 LFS(大文件存储)方案。

add 的反向操作:想撤销暂存,用 git restore --staged 文件;想完全放弃某个文件的暂存和修改,用 git restore 文件。这两个命令 2.7 节会系统讲。这里先记住"add 能去也能回"——它始终是可逆操作,暂存区不是悬崖,大胆用。

跨平台路径分隔符:add 时使用反斜杠还是正斜杠?Git 内部统一用正斜杠(/)表示路径,Windows 用户在终端里也可以直接用正斜杠,避免转义问题。例如 git add src/utils/helper.js 在 Windows 同样成立。

一个心智模型:add 是"选择器"不是"动作"

把 add 放在 Git 命令的全景里看,它和其他命令有一个本质区别:add 不产生新对象、不移动指针、不改历史——它只是在暂存区这个"选择器"上勾选内容。真正产生对象的动作是 commit(写入 Commit/Tree 对象),真正移动指针的是 checkout、merge、reset。理解了这个分层,你就明白为什么"add 错了"永远可救——它没对仓库造成任何不可逆影响,改勾选即可。

这也是为什么 Git 教程几乎都会强调"暂存区是 Git 最独特的设计":别的系统把"选择要提交什么"藏在心里,Git 把它显式化成一个可查看、可修改的区域。善用这个显式化,你的提交历史就能保持干净、原子、可回溯——这是长期项目健康度的重要指标。坚持一段时间后你会发现,写提交信息越来越省力,因为每一条提交都是你亲手挑出来的、边界清楚的一小块工作。

要点速记

  • add 的本质:记录文件快照并登记到索引,不是移动文件。
  • 四种用法:指定文件、目录、全部变化(-A)、交互式部分暂存(-p)。
  • add . 的盲区:不含已删除文件;要连删除一起,用 -A。
  • -p 的威力:把一个文件里的多种修改拆进不同提交。
  • status 三区:to be committed / not staged / untracked 对应 add 的不同阶段。
  • 选择性:提交前先想"该装什么",再决定 add 什么。
  • 对象机制:add 已把内容写成 Blob 对象,commit 只是把它挂进历史。
  • 心智定位:add 是"选择器",可逆、无害,大胆用。

下一节提交快照:git commit 把暂存区固化成历史记录——提交信息怎么写,这里有一整套讲究。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U