本节摘要:
git commit把暂存区固化成一条永久的历史记录——它创建提交对象、生成 SHA-1 标识、更新分支指针。本节讲清提交的本质与四步内部动作,重点落在提交消息的写法上,再演示 -m、编辑器、-a、--amend、-v 五种用法,最后用 git log 查看提交历史。
阅读完本节,你应当能够:
假设你是一个图书管理员,收到一批新书。你的工作不是把它们堆在走廊(那相当于工作区),也不是写张"待入库"清单(那相当于暂存区),而是真正登记入库——给每本书贴上唯一编号、记下归档日期、写清来源和归属书架,让它们从此成为馆藏的一部分,任何人按编号都能找到。
git commit 就是这个"入库动作"。它把暂存区里的内容正式写进仓库的历史,从此这条记录就"钉"在项目的时间线上了——不是临时文件,是永久档案。
有一个动作必须强调:commit 之后,你的提交就有了一个唯一的 SHA-1 哈希,相当于档案编号。这个编号不是随机的,它是根据提交内容(文件快照、作者、时间、父提交)算出来的指纹——内容一变,编号就变。这就是为什么 Git 说"历史不可篡改":想改历史,等于换编号,等于重新造一条历史。这个性质在第 5 章改历史时会把我们折腾得很惨,但现在它是一道可靠的安全锁。
执行 git commit 时,Git 依次完成:
commit 的本质动作就是"把暂存区的快照正式挂进历史"。它不直接看工作区——工作区里没 add 的改动,commit 一概不认。
好奇的话,可以用命令看一个提交的内部结构:
git cat-file -p HEAD
输出大致是这样:
tree 3a4f2c... ← 本次提交时项目的目录结构(Tree 对象) parent e9d1b8... ← 上一个提交(第一个提交没有这一行) author 张三 <zhangsan@example.com> 1700000000 +0800 committer 张三 <zhangsan@example.com> 1700000000 +0800 修复订单超时 bug
这一小段就是 Commit 对象的全部内容:指向一棵 Tree(当时整个项目的目录树)、指向一个父提交(除非是初始提交)、作者与提交者信息、以及提交消息。看到"tree"这行你就明白——一个提交里装着整个项目在那个时刻的完整快照,这正是第 1.4 节"存快照而非差异"的物理形态。而"parent"这行把提交串成链,就是历史本身。

跟着敲一遍,体会 commit 前后的变化:
echo hello > a.txt git add a.txt git status # a.txt 在 "to be committed" git commit -m "添加 a.txt" # 提交 git status # 工作区干净 git log --oneline # 看到第一条提交 echo world >> a.txt # 修改已跟踪文件 git commit -am "扩展 a.txt" # -a 自动暂存并提交 git log --oneline # 两条提交
特别留意第二次提交:-am 一步完成 add + commit,因为 a.txt 是已跟踪文件。如果是新文件,-a 就无能为力了——这也是很多新人困惑"为什么 -a 没提交我的新文件"的原因。
提交消息是 commit 最容易被忽视、也最值得投入的部分。它有三个真实作用:
推荐的提交消息格式是"主题行 + 空行 + 正文":
一个对比:
# 差:看不出意图 update # 好:一眼看懂 修复订单超时 bug 调用支付接口时增加了 5 秒超时上限,并补充了重试逻辑。
主题行与正文的关系,像邮件的标题与正文:标题让人扫一眼决定要不要点开,正文是实质内容。两者都重要,但主题行是门面。
一、最简单:-m 直接给消息
git commit -m "修复登录页按钮错位"
二、多行消息:多次 -m
git commit -m "添加用户中心" -m "含个人信息页与订单列表,后端接口为新增的 profile 路由。"
第二个 -m 的内容会成为正文,两个 -m 之间 Git 自动插空行。
三、打开编辑器写消息
直接 git commit(不带 -m),Git 会打开你配置的默认编辑器,里面预置了 status 输出作参考。写好消息保存退出即提交。
四、-a 跳过 add
git commit -a 会自动暂存已跟踪文件的修改和删除并提交。注意它不处理未跟踪的新文件,新文件仍需先 add。这个选项适合"改的都是老文件、懒得一个个 add"的场景。
git commit -am "统一时间格式"
五、--amend 修补上一次提交
提交后发现消息写错了、或漏了一个小文件,用 --amend 把暂存区与上一次提交合并,形成一条新提交替换旧提交:
git add 忘记的文件 git commit --amend --no-edit
--no-edit 表示沿用原提交消息。重要警告:amend 会改写历史,只适用于尚未推送到远程的提交,推出去再 amend,等于给别人制造混乱(第 5 章细讲)。
六、-v 在编辑器里看 diff
git commit -v 打开编辑器时,在消息模板下方附上本次提交的完整 diff,写消息前先确认"我要提交的到底是什么"。
提交频率没有标准答案,但有两条经验:
| 提交习惯 | 优点 | 代价 |
|---|---|---|
| 小步频繁提交 | 历史精细、易回滚、易审查 | 提交信息要写得多 |
| 大块低频提交 | 省事 | 历史粗糙、难定位、难回滚 |
我的判断:新手从"小步频繁"练起,虽然初期觉得啰嗦,但这是最能吃透 Git 价值的习惯。等你有经验了,自然会找到适合自己的粒度。
举一个具体的粒度对比。假设你在实现"用户登录"功能,涉及接口、页面、样式三个文件:
前者的"代价"是三次提交信息要写三句,后者的"代价"是排查问题时多花几倍时间。怎么选,答案很清楚。
每次提交前,快速过三问:
git status:确认要提交的内容都在暂存区,没有漏网之鱼?git diff --cached:看一眼已暂存的改动,确认没有手滑?这三问只要一分钟,但能省下后面无数"我怎么把那个文件提交进去了"的后悔。补一句心理建设:提交前花一分钟检查,不是强迫症,而是把"改错了"的成本从"污染历史"降低到"未遂事件"——检查发生在提交之前,一切都还来得及。
⚠️ 常见坑:
git commit -a会把所有已跟踪文件的修改一次全提交,包括你可能不想一起提交的改动。用 -a 前先 git status 确认。
💡 关键直觉:把提交想成给项目的"里程碑立碑"。碑文(提交消息)写得好,后人(包括三个月后的你)扫一眼就知道这座碑纪念了什么;碑文写"update",这座碑就跟没立一样。
"git commit 后想改消息怎么办?" 没推远程就 git commit --amend 改;已推远程,就再推一次新提交修改,或用 revert(第 5 章讲)。记住 amend 只适合本地未推送的提交。
"提交了不该提交的文件怎么办?" 分两步:用 restore --staged 或 reset 把它从暂存区撤下(如果还没提交);已提交则用 rm 或改 .gitignore 后新提交移除。注意:已提交进历史的文件,删除它需要新提交"反向操作",历史里仍会留下它——彻底清除历史里的文件要重写历史,属第 5 章范畴。
"提交信息用中文还是英文?" 团队统一即可,个人项目随你喜欢。关键是格式一致、能说清"为什么"。混合语言反而最乱。
"空提交(什么都没改)能提交吗?" 默认不能——Git 会拒绝没有改动的提交。如果你确实需要(比如触发 CI),用 git commit --allow-empty。
"commit 会提交没 add 的新文件吗?" 不会。commit 只认暂存区,工作区的新文件没 add 就永远进不了这次提交。这是很多新人"我明明 commit 了怎么文件没进仓库"的原因。
"update" 式:两个字,什么都没说。update 了什么?为什么?这类消息让历史彻底失去检索价值,属于最典型的无效提交信息。
"fix" 式:比 update 好一点,但仍缺关键信息。修了什么 bug?修之前是什么行为?写"修复订单金额溢出导致负数"就具体得多。
"小改" 式:看似诚实,实则无用。"小改配置"和"调整 42 行字体大小"的信息量完全不同。
提交信息里塞代码:把一大段 diff 或 shell 命令贴进提交消息。diff 在 git show 里能看到,重复贴进去只是噪音。
解决办法其实不复杂:把提交消息当成给未来同事(包括未来的你)的一封短信,主题行说清"干了什么",正文说清"为什么"。一次训练,终身受益——而且写多了之后,你会发现它其实是在逼你把每个改动想清楚,这本身就是一种代码质量提升。
下一节查看状态与历史:
git status回答"现在怎么了",git log回答"过去怎么了"。