【Git】从入门到精通(五)—— git add:将文件加入暂存区

作者:全栈陈序员 系列:《Git版本控制从入门到精通》 适用人群:零基础到入门进阶 阅读时间:约 18 分钟


写在前面

上一篇我们彻底搞懂了 Git 的三大区域。其中"工作区 → 暂存区"这一步,靠的就是 git add。

git add 看起来简单------不就是"加文件嘛"------但它的用法远比新手想象的丰富:

  • 加单个文件、加多个文件、加整个目录、加所有改动;
  • 只加修改、只加新文件、包含删除;
  • 部分暂存 (git add -p):同一个文件改了10处,只暂存其中3处;
  • 通配符、与 .gitignore 的关系、强制添加被忽略的文件;
  • 查看和撤销暂存。

本篇会把 git add 的方方面面讲透,让你彻底驾驭这个"工作区到暂存区"的关键命令。


一、git add 的作用

一句话 :git add 把工作区的改动(新增、修改、删除)记录到暂存区 ,为下一次 git commit 做准备。

复制代码
工作区 ──git add──→ 暂存区 ──git commit──→ 版本库

关键认知:

  1. git add 不提交,只是"放进购物车";
  2. git add 后文件仍在工作区(工作区文件不会被移走),只是它的改动被"标记"到了暂存区;
  3. 同一个文件可以多次 git add,后一次会覆盖前一次的暂存内容(暂存区记录的是最新一次 add 时的快照)。

二、基本用法

2.1 添加单个文件

复制代码
$ echo "print('hello')" > main.py
$ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	main.py

$ git add main.py
$ git status
On branch main
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   main.py

main.py 从 Untracked 变成 new file(待提交)。

2.2 添加多个文件

一次性添加多个文件,空格分隔:

复制代码
$ echo "x = 1" > config.py
$ echo "def util(): pass" > utils.py
$ git add config.py utils.py
$ git status
On branch main
Changes to be committed:
	new file:   config.py
	new file:   main.py        # 之前已 add
	new file:   utils.py

2.3 添加当前目录所有变更:git add .

复制代码
$ git add .
$ git status

. 表示当前目录及其子目录的所有变更(新增、修改、删除)。这是最常用的写法之一。

注意 :在 Git 2.0+,git add . 只添加当前目录及子目录 的变更,不包括仓库其他部分。如果你在子目录执行 git add .,只会添加该子目录下的变更。

2.4 添加所有变更:git add -A(或 --all)

复制代码
$ git add -A

-A / --all 添加整个仓库的所有变更(无论你在哪个子目录执行),包括新增、修改、删除。

git add . vs git add -A 的区别:

命令 作用范围 包含变更类型
git add . 当前目录及子目录 新增、修改、删除(Git 2.0+)
git add -A 整个仓库 新增、修改、删除
git add -u 整个仓库(仅已追踪文件) 修改、删除(不含新增)

举例 :你在 src/ 子目录下,仓库根还有改动的 README.md:

  • git add .:只 add src/ 下的变更,不 add README.md;
  • git add -A:add 整个仓库的变更,包括 README.md。

2.5 只添加已追踪文件的修改:git add -u

复制代码
$ git add -u

-u / --update 只对已被 Git 追踪的文件 生效,添加它们的修改和删除,不会添加新文件(Untracked)。

场景:你改了几个老文件,又新建了几个文件,但新文件还没确定要不要提交,只想先把老文件的改动暂存:

复制代码
$ git status
On branch main
Changes not staged for commit:
	modified:   main.py        # 已追踪,改了
Untracked files:
	experimental.py            # 新文件,还没想好要不要提交

$ git add -u
$ git status
On branch main
Changes to be committed:
	modified:   main.py        # 已暂存
Untracked files:
	experimental.py            # 仍未追踪,没被 add

三、git add 的各种场景

我们用一组连贯的演示覆盖所有场景。

3.1 场景一:添加新文件

复制代码
$ echo "# 新文档" > NEW.md
$ git add NEW.md
$ git status
Changes to be committed:
	new file:   NEW.md

新文件首次 add 后,状态显示 new file。

3.2 场景二:添加修改的文件

复制代码
$ echo "新增一行" >> main.py
$ git status
Changes not staged for commit:
	modified:   main.py

$ git add main.py
$ git status
Changes to be committed:
	modified:   main.py

已追踪文件被修改后,状态是 modified,add 后变成待提交的 modified。

3.3 场景三:添加删除的文件

复制代码
$ git rm utils.py
rm 'utils.py'

$ git status
Changes to be committed:
	deleted:    utils.py

用 git rm 删除文件时,删除会自动加入暂存区。

但如果你用普通 rm(不带 git)删除:

复制代码
$ rm config.py
$ git status
Changes not staged for commit:
	deleted:    config.py      # 删除还没暂存

$ git add config.py           # 或 git add -u
$ git status
Changes to be committed:
	deleted:    config.py      # 删除已暂存,下次commit会生效

关键 :git add <已删除的文件名> 能把"删除"这个动作加入暂存区。这是因为 git add 的本质是"让暂存区与工作区同步",工作区没了,暂存区也标记为没了。

3.4 场景四:添加重命名的文件

复制代码
$ git mv main.py app.py
$ git status
Changes to be committed:
	renamed:    main.py -> app.py

git mv 会自动把重命名加入暂存区。

如果用普通 mv:

复制代码
$ mv app.py entry.py
$ git status
Changes not staged for commit:
	deleted:    app.py
Untracked files:
	entry.py

$ git add -A            # 或 git add app.py entry.py
$ git status
Changes to be committed:
	renamed:    app.py -> entry.py

Git 足明地识别出"删除 app.py + 新增 entry.py"其实是重命名 (基于内容相似度),显示为 renamed。


四、部分暂存:git add -p(高级但超实用)

这是 git add 最强大的功能之一,每个 Git 用户都该会。

4.1 场景

你在一个文件里改了多处,但它们属于不同的逻辑改动,想分成多次提交。比如:

复制代码
# app.py
def login(user):
    # 修复了登录Bug
    if user is None:          # 改动1:修Bug
        return False
    session.create(user)

def logout(user):             # 改动2:新增功能
    session.destroy(user)

# 顺便加了个调试打印
print("debug: app loaded")    # 改动3:调试代码,不想提交

你想:第一次提交只提交"修Bug",第二次提交"新增logout",调试代码不提交。这时候就要 git add -p。

4.2 交互式界面

复制代码
$ git add -p app.py
diff --git a/app.py b/app.py
index 1a2b3c4..5d6e7f8 100644
--- a/app.py
+++ b/app.py
@@ -1,3 +1,5 @@
 def login(user):
+    if user is None:
+        return False
     session.create(user)

(1/3) Stage this hunk [y,n,q,a,d,s,e,?]?

Git 把改动分成一个个 hunk(代码块),逐块问你"要不要暂存这一块"。

4.3 交互选项详解

选项 作用
y yes,暂存这个 hunk
n no,不暂存这个 hunk
q quit,退出,不再处理后续 hunk
a all,暂存这个 hunk 及后续所有 hunk
d don't,不暂存这个及后续所有 hunk
s split,把这个 hunk 拆成更小的 hunk(如果 Git 分得太粗)
e edit,手动编辑这个 hunk(高级,可精确到行)
? 显示帮助

4.4 实战示例

复制代码
$ git add -p app.py
diff --git a/app.py b/app.py
@@ -1,3 +1,5 @@
 def login(user):
+    if user is None:
+        return False
     session.create(user)
(1/3) Stage this hunk [y,n,q,a,d,s,e,?]? y      # 暂存修Bug的改动

@@ -4,3 +6,6 @@
 def logout(user):
+    session.destroy(user)
+
+print("debug: app loaded")
(2/3) Stage this hunk [y,n,q,a,d,s,e,?]? s      # 这块混了logout和debug,拆开

Split into 2 hunks.
@@ -4,3 +6,4 @@
 def logout(user):
+    session.destroy(user)
(2/3) Stage this hunk [y,n,q,a,d,s,e,?]? y      # 暂存logout

@@ -7,1 +9,1 @@
+print("debug: app loaded")
(3/3) Stage this hunk [y,n,q,a,d,s,e,?]? n      # 不暂存debug代码

结果:暂存区里只有"修Bug"和"logout"两处改动,debug 打印留在工作区未暂存。

复制代码
$ git status
On branch main
Changes to be committed:
	modified:   app.py        # 部分内容已暂存
Changes not staged for commit:
	modified:   app.py        # 同一文件还有未暂存的改动(debug)

现在可以分两次提交:

复制代码
$ git commit -m "fix: 修复login空指针Bug"
$ git add app.py             # 把剩余改动(logout)暂存
$ git commit -m "feat: 新增logout功能"
# debug代码仍留在工作区,未提交

git add -p 的价值 :让你的提交逻辑清晰、便于审查、便于回滚,是职业 Git 用户的标志技能。


五、git add 的通配符

5.1 按扩展名添加

复制代码
# 添加所有 Python 文件
$ git add "*.py"

# 添加所有 Markdown 文件
$ git add "*.md"

注意 :通配符要加引号,否则会被 Shell 先展开,可能不符合预期。加引号后由 Git 自己匹配,行为更可控。

5.2 按目录添加

复制代码
# 添加 src 目录下所有变更
$ git add src/

# 添加 src 及 tests 目录
$ git add src/ tests/

5.3 排除某些文件

Shell 通配符配合:

复制代码
# 添加所有 .py 但不包括 test_ 开头的
$ git add "*.py"
$ git reset HEAD "test_*.py"     # 暂存后再取消暂存测试文件

更优雅的方式是配合 .gitignore,下节讲。

5.4 注意事项

  1. Shell 会先展开通配符 :git add *.py(不加引号)时,Shell 把 *.py 展开成当前目录所有 .py 文件名再传给 Git,不会递归子目录 。加引号 git add "*.py" 让 Git 处理,会递归。
  2. . 始终表示当前目录 :在子目录执行 git add . 只影响该子目录。
  3. 通配符不匹配以点开头的文件 (如 .env):git add * 不会加 .env,要 git add .env 显式加,或 git add -A。

六、git add 与 .gitignore 的关系

6.1 被忽略的文件不会被 add

.gitignore 里声明的文件,git add 会自动跳过:

复制代码
$ echo "secret" > .env
$ echo ".env" > .gitignore
$ git add .env
The following paths are ignored by one of your .gitignore files:
.env
hint: Use -f if you really want to add them.
hint: Disable this hint with "git config advice.addIgnoredFile false"

Git 拒绝添加,并提示用 -f 强制。

6.2 强制添加被忽略的文件:git add -f

复制代码
$ git add -f .env
$ git status
Changes to be committed:
	new file:   .env

-f / --force 强制添加,即使被 .gitignore 匹配。

慎用 !通常 .env、密钥、配置文件被忽略是有原因的。强制添加可能把敏感信息提交进仓库,造成安全事故。如果确实要提交一个被忽略的文件,先想清楚为什么它被忽略,是否真的该提交。

6.3 检查文件为何被忽略

复制代码
$ git check-ignore -v .env
.gitignore:1:.env	.env

git check-ignore -v 显示是哪个 .gitignore 文件的哪一行规则忽略了该文件,排查"为什么 add 不进去"很有用。

6.4 已追踪文件被加入 .gitignore 后

如果一个文件已经被 Git 追踪 ,后来才把它加进 .gitignore,Git 仍然会追踪它的改动 (.gitignore 只对未追踪文件生效)。

复制代码
$ git add config.js
$ git commit -m "add config"
$ echo "config.js" >> .gitignore
$ echo "new" >> config.js
$ git status
Changes not staged for commit:
	modified:   config.js      # 仍然被追踪!.gitignore 没生效

要让 .gitignore 对已追踪文件生效,需先取消追踪:

复制代码
$ git rm --cached config.js    # 从暂存区移除,但保留工作区文件
$ git commit -m "停止追踪 config.js"
# 之后 config.js 的改动才真正被忽略

七、查看暂存区内容

git add 之后,怎么确认暂存区里到底有什么?

7.1 git status

最常用,显示工作区和暂存区的状态概览:

复制代码
$ git status
On branch main
Changes to be committed:
	new file:   a.py
	modified:   b.py
	deleted:    c.py

Changes not staged for commit:
	modified:   d.py

Untracked files:
	e.py
  • Changes to be committed:已暂存,下次 commit 会提交;
  • Changes not staged for commit:改了但没暂存;
  • Untracked files:新文件,没追踪。

7.2 git diff --staged(查看暂存的具体内容)

复制代码
$ git diff --staged
diff --git a/a.py b/a.py
new file mode 100644
index 0000000..e69de29
diff --git a/b.py b/b.py
index 1111111..2222222 100644
--- a/b.py
+++ b/b.py
@@ -1,3 +1,4 @@
 line1
 line2
 line3
+line4

--staged(或 --cached,两者等价)显示暂存区与版本库 的差异,即"下次 commit 会提交什么"。提交前务必看一眼,避免误提交。

7.3 git ls-files(列出暂存区文件)

复制代码
# 列出暂存区所有文件
$ git ls-files
.gitignore
a.py
b.py
README.md

# 只列出暂存区中修改过的文件
$ git ls-files -m
b.py

# 列出未追踪文件
$ git ls-files --others --exclude-standard
e.py

git ls-files 直接操作暂存区,比 git status 更底层,适合脚本使用。


八、撤销暂存(取消 git add)

git add 加错了怎么办?把改动从暂存区"拿出来"。

8.1 git restore --staged(推荐,Git 2.23+)

复制代码
$ git add wrong.py
$ git restore --staged wrong.py
$ git status
Untracked files:
	wrong.py        # 回到未暂存状态

git restore --staged <file> 把文件从暂存区移除,改动保留在工作区,不会丢失内容。

8.2 git reset HEAD(传统写法)

复制代码
$ git reset HEAD wrong.py
# 效果同上

git reset HEAD <file> 是老写法,git restore --staged 是新写法(更语义化)。两者效果相同。

8.3 撤销全部暂存

复制代码
$ git restore --staged .
# 或
$ git reset HEAD

把暂存区清空,所有改动退回工作区。

8.4 注意:撤销暂存 ≠ 丢弃改动

复制代码
# 撤销暂存(改动还在工作区,安全)
$ git restore --staged file.py

# 丢弃工作区改动(改动会丢失,危险!)
$ git restore file.py

一字之差,天壤之别。前者只是"不打算提交了",后者是"我不要这个改动了"。新手极易混淆,务必分清。


九、常见问题FAQ

Q1:git add . 和 git add -A 到底什么区别?

A:

  • git add .:添加当前目录及子目录的变更(Git 2.0+)。在子目录执行只影响该子目录。
  • git add -A:添加整个仓库所有变更,无论你在哪执行。
  • 在仓库根目录执行时,两者效果相同;在子目录执行时不同。

Q2:为什么 git add 后文件还在工作区?

A:git add 不是"移动"文件,而是把改动的快照记录到暂存区。工作区文件原封不动,你还能继续编辑。暂存区记录的是"add 那一刻的快照",之后你再改,需要再 add 才能更新暂存区。

Q3:空目录能 git add 吗?

A:不能 。Git 不追踪空目录,只追踪文件。要保留一个空目录,惯例是放一个 .gitkeep 空文件:

复制代码
$ mkdir empty-dir
$ touch empty-dir/.gitkeep
$ git add empty-dir/.gitkeep

Q4:git add 同一个文件多次会怎样?

A:后一次覆盖前一次的暂存内容。暂存区始终记录最后一次 add 时的快照。

复制代码
$ echo "v1" > f.txt && git add f.txt      # 暂存 "v1"
$ echo "v2" > f.txt && git add f.txt      # 暂存更新为 "v2"
$ git commit -m "test"                     # 提交的是 "v2"

Q5:git add -p 时 hunk 太大,怎么拆细?

A:在交互提示时按 s(split)拆分,或按 e(edit)手动编辑 hunk,删掉不要的行(注意保持 diff 格式正确)。

Q6:误 git add 了大文件/敏感文件,还没 commit,怎么办?

A:还没 commit 就好办,取消暂存即可:

复制代码
$ git restore --staged 敏感文件

如果已经 commit ,需用 git reset --soft HEAD~1 撤销提交(保留改动),再取消暂存,或用 BFG 清理历史。

Q7:git add 会改变文件内容吗?

A:不会。git add 只读取工作区文件内容,存到暂存区(.git/index 和 .git/objects),工作区文件原样不动。

Q8:如何只暂存新增文件,不暂存修改的文件?

A:没有直接选项,可配合:

复制代码
$ git add .                              # 全加
$ git reset HEAD $(git diff --name-only --diff-filter=M HEAD)  # 取消已修改文件的暂存

或用 git ls-files --others --exclude-standard | xargs git add 只加未追踪文件。


十、进阶技巧

10.1 用 git add -i 进入交互式菜单

除了 -p(部分暂存),还有 -i(交互式菜单),提供更全面的交互操作:

复制代码
$ git add -i
*** Commands ***
  1: status       2: update       3: revert       4: add untracked
  5: patch        6: diff         7: quit         8: help
What now>

各选项含义:

  • status:显示暂存状态;
  • update:选择已追踪文件来暂存修改;
  • revert:取消暂存;
  • add untracked:选择未追踪文件加入暂存;
  • patch:进入部分暂存模式(同 -p);
  • diff:查看暂存区与版本库的差异。

交互式菜单适合不熟悉命令参数的新手,熟练后直接用 -p、-A、-u 更快。

10.2 用 git add 暂存意图(intent to add)

复制代码
$ git add -N newfile.py

-N / --intent-to-add 把文件标记为"打算添加",但暂存区里内容为空。这样后续 git diff 能显示这个文件的改动(像已追踪文件一样),便于边写边看差异,最后再正式 git add。

10.3 暂存时忽略可执行位变化

跨平台时,文件可执行权限(chmod +x)的变化可能造成无谓改动:

复制代码
$ git add --chmod=+x script.sh    # 暂存并标记为可执行
$ git add --chmod=-x data.json    # 暂存并标记为不可执行

10.4 理解 git add 的本质

从底层看,git add 做了两件事:

  1. 把工作区文件的内容写入 .git/objects,生成一个 blob 对象(内容寻址存储);
  2. 更新 .git/index(暂存区),记录"这个文件名对应这个 blob 哈希"。

所以 git add 并不"复制文件到暂存区",而是把内容存进对象库、在索引里记一笔映射。这也是为什么 git add 后文件还在工作区------它本来就没被移动,只是被"登记"了。理解了这个本质,很多疑惑就迎刃而解:比如为什么 git add 后再改文件,暂存区不会自动更新------因为索引里记的是上次那个 blob 的哈希,你改了文件但没重新 add,索引还指向旧 blob。

这个底层认知也能解释为什么 Git 速度极快:git add 只是算哈希、写对象、更新索引,全是本地哈希表操作,没有网络、没有复杂数据库,所以即便大型项目也能瞬间完成。


十一、最佳实践

  1. 提交前必看 git diff --staged:确认暂存的内容正是你想提交的,避免误提交调试代码、密钥、print 语句。
  2. 优先用 git add <具体文件> 而非 git add . :精确添加,减少误操作。git add . 适合你明确知道当前所有改动都该提交时。
  3. 善用 git add -p 做部分暂存:让提交逻辑清晰,一个提交只做一件事。
  4. 第一次 add 前先建 .gitignore:避免误加编译产物、IDE 文件、密钥。
  5. 别用 git add -f 强加被忽略的文件,除非你确切知道为什么。
  6. 撤销暂存用 git restore --staged ,别和 git restore(丢弃改动)搞混。
  7. 小步快跑:改一点 add 一点 commit 一点,而不是攒一大堆改动一次性提交。小提交便于审查、回滚、定位问题。
  8. 提交信息要配得上提交内容 :git add -p 让你能精确控制"这次提交做了什么",提交信息就能写准。

如果本文对你有帮助,欢迎点赞、收藏、关注。我是全栈陈序员,我们下篇见。

相关推荐
KKKlucifer2 小时前
国内数据安全服务的产品智能化转型:大模型赋能下的技术能力重构分析
大数据·数据库·重构
故七月3 小时前
AI 答案里的竞品共现:怎么看出赛道格局
大数据·人工智能
全栈陈序员3 小时前
【Git】从入门到精通(七)—— git status:查看仓库状态
大数据·git·elasticsearch
fengye2071614 小时前
第三章 编译cpython
大数据·elasticsearch·搜索引擎
大大大大晴天4 小时前
每天认识一个组件:Apache Flink
大数据
涛思数据(TDengine)5 小时前
从工业本体到 AI 原生:TDengine 出席 2026 QCon 分享工业数据基础设施路径
大数据·数据库·人工智能·tdengine·工业
Gu_WenYun6 小时前
从主题定价到订单定价:机器人基金的产业链观察
大数据·人工智能·金融
其实防守也摸鱼6 小时前
信创安全竞赛知识总结与题库
大数据·网络·人工智能·算法·安全·php
刀笔财经6 小时前
新时达的选择:先造血,再谈利润
大数据