git add 和 git commit:如何正确保存更改
在 Git 中保存更改分两步:git add 把需要的修改放入暂存区(staging area),git commit -m "说明" 把它们作为一个快照写入历史。git status 显示哪些内容已修改、哪些已暂存。修改上一次提交可以使用 git commit --amend。
Git 中没有“保存”按钮。更改分两步记录:首先 git add 挑选哪些修改将进入下一个快照,然后 git commit 把这个快照写入历史。两者之间的中间区域叫作暂存区(index,或 staging area)。很多人多年来都没有真正理解它,而正是它让每次提交都有意义。
文件的三个状态:工作目录、暂存区、历史
每一处修改都会经过三个区域:
- 工作目录:你在编辑器里修改文件。
- 暂存区(stage):
git add把文件的当前版本放进下一次提交的草稿。 - 历史:
git commit把暂存区的内容变成一次带有作者、日期和说明的提交。
一个方便的比喻是包裹。工作目录是放满东西的房间,暂存区是敞开的箱子,你把想寄出的东西放进箱子,提交则是封好并签名的包裹。装什么由你决定,并不必一次全部寄出。
暂存区的作用
设想一下:一小时里你修复了登录的一个 bug,顺手改了 README 中的一个错别字,又开始了一个新功能。如果没有暂存区,这些内容会全部混进一次名为“杂项”的提交里。有了暂存区,你可以做三次独立的提交,每一次都容易理解、容易评审,也能单独回滚而不影响其他部分。
git status:哪些内容已修改,哪些已暂存
在每次 add 和 commit 之前,最好先看一眼 git status。这是最安全的 Git 命令,它不会改变任何东西。
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/auth.js
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
src/profile.js
- Changes to be committed:已经在暂存区,将进入提交;
- Changes not staged for commit:已修改,但尚未暂存;
- Untracked files:新文件,Git 还不知道它们的存在。
文件较多时,短格式更方便:
$ git status -s
M src/auth.js
M README.md
?? src/profile.js
第一列表示暂存区中的状态,第二列表示工作目录中的状态。左边的 M 表示“已修改并已暂存”,右边的 M 表示“已修改但未暂存”,?? 表示未被跟踪。
git add:把修改加入暂存区
| 命令 | 添加的内容 |
|---|---|
git add file.js | 指定的文件 |
git add src/ | 文件夹中所有的修改 |
git add . | 当前文件夹及其子目录中的所有修改,包括新文件和已删除的文件 |
git add -A | 整个仓库中的所有修改 |
git add -u | 只包括已被跟踪的文件,不处理新文件 |
git add -p | 交互式,按代码块(hunk)逐个选择 |
git add '*.css' | 符合模式的文件 |
git add 的别名是 git stage,它们是同一个命令,因此会说“stage”(暂存)某个文件。
git add -p:只添加文件的一部分
这是一个被低估的技巧。如果同一个文件中混杂了两处不相关的修改,git add -p 会逐块展示修改,并询问是否添加:
$ git add -p src/auth.js
@@ -12,7 +12,7 @@ function login(user) {
- if (user.password = input) {
+ if (user.password === input) {
(1/2) Stage this hunk [y,n,q,a,d,s,e,?]?
y 表示添加,n 表示跳过,s 表示把代码块拆得更细,q 表示退出。这样,同一个文件就能拆成两次整洁的提交。
如何检查将要进入提交的内容
git diff # 尚未加入暂存区的修改
git diff --staged # 已在暂存区、将进入提交的内容
常见的困惑是:执行 git add 之后,普通的 git diff 没有任何输出。这并不意味着修改丢失了,它们已经在暂存区中,请使用 git diff --staged(等同于 --cached)查看。
如何把文件移出暂存区
git restore --staged file.js
文件中的修改仍然保留,只是不再准备提交。在旧版本的 Git 中,这一操作使用 git reset HEAD file.js,现在依然可用。
git commit:提交更改
git commit -m "Fix password check on login"
[main 3f2a9c1] Fix password check on login
1 file changed, 1 insertion(+), 1 deletion(-)
输出中包括分支名、新提交的短哈希以及统计信息。提交只包含暂存区的内容。如果执行 git add 之后你又修改了文件,新的修改不会进入提交,除非你再次添加它们。
git commit 的常用参数
| 命令 | 作用 |
|---|---|
git commit | 打开编辑器,用于编写多行说明 |
git commit -m "..." | 直接在命令中写说明 |
git commit -am "..." | 添加所有已修改的已跟踪文件并提交;新文件不会被包括 |
git commit -v | 在编辑器中写说明时同时显示 diff |
git commit --amend | 覆盖上一次提交 |
git commit --allow-empty -m "..." | 空提交,例如用于重新触发 CI |
如果默认编辑器是 Vim,而你不知道怎么退出:输入说明,按 Esc,然后输入 :wq 并回车。更换编辑器的方法:git config --global core.editor "code --wait",或者使用 nano。
如何写好提交说明
说明会在半年后被人翻看,用来查找 bug 的来源。它应该回答“改了什么、为什么改”,而不是“我今天做了什么”。
- 第一行控制在 50–72 个字符以内,概括修改的核心;
- 使用祈使语气或中性描述:“Add email validation”“修复登录后的跳转”;
- 必要时空一行,写明细节:为什么这样做,有哪些限制;
- 一次提交只做一件逻辑上的事。
不好的说明:fix、changes、wip、asdf。好的说明:Fix cart crash when the product list is empty。许多团队采用 Conventional Commits 格式:feat: ...、fix: ...、docs: ...。
git commit --amend:修改上一次提交
说明中有错别字,或者忘了添加某个文件时,不必再做一次“补充文件”的提交,amend 就是为此而设的。
修改说明
git commit --amend -m "Fix password check on login"
添加漏掉的文件
git add forgotten.js
git commit --amend --no-edit
参数 --no-edit 保留原来的说明。
关于 amend 的重要事项
--amend 并不是“编辑”提交,而是创建一个新提交,哈希不同,并用它替换旧的提交。只要提交还只在你这里,这样做就是安全的。如果你已经执行过 git push,amend 之后普通的 push 会被拒绝,需要使用 git push --force-with-lease。在其他人也在工作的共享分支上,请不要这样做:你会改写他们已经基于其开展工作的历史。
如果误用了 amend,可以在 git reflog 中找到旧提交,并通过 git reset --soft HEAD@{1} 回到它。
常见错误
“nothing to commit, working tree clean”
没有任何修改:要么已经全部提交,要么你在另一个文件夹里修改文件。请检查 git status 和项目路径。
“no changes added to commit”
文件已修改,但没有加入暂存区。执行 git add,或对已跟踪的文件使用 git commit -am。
“Please tell me who you are”
未设置姓名和邮箱。只需执行一次 git config --global user.name "Your Name" 和 git config --global user.email "you@example.com"。
提交了不该提交的内容
如果提交还没有推送,可以撤销它并保留修改在暂存区:git reset --soft HEAD~1。然后用 git restore --staged 移除多余的部分,重新提交。如果密钥等机密信息进入了提交,应视为已泄露,并立即重新生成。
git add . 把垃圾文件也加进来了
提前配置好 .gitignore,并在提交前查看 git status。已经被跟踪的文件,只有执行 git rm --cached 文件 之后才会停止被跟踪。
提交的实际内容
提交保存的并不是与上一个版本的差异,而是整个项目的快照:指向文件树的引用、指向父提交的引用、作者、日期和说明。未改变的文件不会被复制,Git 只引用已经保存的对象,因此历史占用的空间很小。
提交哈希(例如 3f2a9c1)由上述全部内容计算得出。哪怕只修改说明中的一个字母,哈希也会不同。这就是为什么 --amend 和 rebase 会创建新提交,而不是修改旧提交,也是为什么改写过的历史会与同事手中的副本产生冲突。
可以用下面的命令简要查看历史:
git log --oneline --graph -10
每一行都是一次提交。如果不打开 diff 也能看懂历史中的每条说明,说明你的做法是正确的。
日常工作流程
git status # 有哪些变化
git diff # 查看修改
git add -p # 挑选需要的内容
git diff --staged # 检查将要提交的内容
git commit -m "Clear description"
git log --oneline -5 # 确认提交已经生成
git push # 推送到服务器
提交只会在本地保存修改。要让它们出现在 GitHub 或其他服务器上,需要执行 git push。
总结
git add 在暂存区中组装下一个快照,git status 与 git diff --staged 显示其中将包含什么,git commit 把快照写入历史。请做小而合乎逻辑的提交,并附上清晰的说明;对于上一次提交的小修改,在 push 之前使用 git commit --amend。暂存区以及 add → commit 的两步流程,在 Git 课程中有实践讲解。
常见问题
应该多久提交一次?
每当完成一个逻辑上的修改、且项目处于可工作的状态时就提交。宁可多做几次小提交,也不要在一天结束时做一次巨大的提交。
Git 中有 git save 命令吗?
没有这个命令。保存就是 git add 和 git commit,暂时搁置未完成的工作可以使用 git stash。
如何查看某次提交的内容?
命令 git show 哈希值 会显示作者、说明以及该提交的全部修改。如果不写哈希值,它显示的是最近一次提交。
git add . 和 git add -A 有什么区别?
在现代版本的 Git 中,两者都会添加新增、修改和删除的文件。区别在于范围:git add . 作用于当前文件夹及其子目录,git add -A 作用于整个仓库。
能修改较早的提交而不是最后一次吗?
可以,使用交互式 rebase:git rebase -i 父提交哈希,然后把目标提交标记为 edit 或 reword。这只适用于尚未推送到共享分支的提交。