作者:全栈陈序员 系列:《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──→ 版本库
关键认知:
git add不提交,只是"放进购物车";git add后文件仍在工作区(工作区文件不会被移走),只是它的改动被"标记"到了暂存区;- 同一个文件可以多次
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 .:只 addsrc/下的变更,不 addREADME.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 注意事项
- Shell 会先展开通配符 :
git add *.py(不加引号)时,Shell 把*.py展开成当前目录所有 .py 文件名再传给 Git,不会递归子目录 。加引号git add "*.py"让 Git 处理,会递归。 .始终表示当前目录 :在子目录执行git add .只影响该子目录。- 通配符不匹配以点开头的文件 (如
.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 做了两件事:
- 把工作区文件的内容写入
.git/objects,生成一个 blob 对象(内容寻址存储); - 更新
.git/index(暂存区),记录"这个文件名对应这个 blob 哈希"。
所以 git add 并不"复制文件到暂存区",而是把内容存进对象库、在索引里记一笔映射。这也是为什么 git add 后文件还在工作区------它本来就没被移动,只是被"登记"了。理解了这个本质,很多疑惑就迎刃而解:比如为什么 git add 后再改文件,暂存区不会自动更新------因为索引里记的是上次那个 blob 的哈希,你改了文件但没重新 add,索引还指向旧 blob。
这个底层认知也能解释为什么 Git 速度极快:git add 只是算哈希、写对象、更新索引,全是本地哈希表操作,没有网络、没有复杂数据库,所以即便大型项目也能瞬间完成。
十一、最佳实践
- 提交前必看
git diff --staged:确认暂存的内容正是你想提交的,避免误提交调试代码、密钥、print 语句。 - 优先用
git add <具体文件>而非git add .:精确添加,减少误操作。git add .适合你明确知道当前所有改动都该提交时。 - 善用
git add -p做部分暂存:让提交逻辑清晰,一个提交只做一件事。 - 第一次 add 前先建
.gitignore:避免误加编译产物、IDE 文件、密钥。 - 别用
git add -f强加被忽略的文件,除非你确切知道为什么。 - 撤销暂存用
git restore --staged,别和git restore(丢弃改动)搞混。 - 小步快跑:改一点 add 一点 commit 一点,而不是攒一大堆改动一次性提交。小提交便于审查、回滚、定位问题。
- 提交信息要配得上提交内容 :
git add -p让你能精确控制"这次提交做了什么",提交信息就能写准。
如果本文对你有帮助,欢迎点赞、收藏、关注。我是全栈陈序员,我们下篇见。