☰
Git 提交与暂存区详解:add、commit、status、stash 核心机制
2026/10/27 11:27:05 网站建设 项目流程

提交这件事,看着简单,实际上是 Git 里最容易翻车的一环。我见过太多人用了两三年 Git,日常就靠git add .加git commit -m "update"这两条命令撑着,一旦碰上“提交早了想合并回去”“文件明明在目录里却不出现在待提交列表”“切分支切到一半发现代码没存”这类场景,就开始翻收藏夹里的命令大全,翻完还是不敢下手。这篇文章把git add、git commit、git status、git stash这四条命令掰开讲一遍,顺带把提交散列值、提交历史的结构、多次提交怎么组织、以及.gitignore忽略规则这几块串起来。它适合已经能跑通基本流程、但对“为什么这么设计”和“出问题怎么救”还一头雾水的朋友,也适合想把自己提交历史整理干净的中高级使用者。读完之后你至少应该能做到:清楚地知道每一次 commit 里到底装了什么,能看懂那串四十位字符代表什么,以及在误操作之后知道从哪儿把东西捞回来。

1. 提交的底层设计:为什么 Git 要多出一个暂存区

1.1 工作区、暂存区、版本库三者的分工

刚接触 Git 的人几乎都会有一个疑问:我改了文件,为什么不能直接提交,非要先add一下?这个疑问的答案,藏在 Git 的三层结构里。

工作区是你眼睛能看到、编辑器能打开的那份文件,是当前真实存在的目录内容。暂存区(index,也叫索引)是一份二进制文件,放在.git/index,它记录的是一棵“下一次提交长什么样”的目录树快照。版本库(.git/objects那一堆)存放的是已经固化成对象的历史快照。这三层的关系不是一条直线,而是一个收口的过程:工作区的改动先被挑选进入暂存区,暂存区再被整体冻结成一次提交。

这个中间层的存在,让“我改了十个文件,但这次只想提交其中三个”这种需求变得极其自然。如果没有暂存区,你要么全提交,要么就得手动备份其他七个文件,想想都累。

1.2 为什么非要一个暂存区,直接提交不行吗

有人会说,别的版本管理工具没有暂存区也能用。这话没错,但代价是“提交”这个动作失去了一次人为筛选的机会。

暂存区的本质是一次提交的草稿区。你在里面反复add、restore --staged、再add,这个过程不产生任何历史记录,怎么折腾都不留痕。等到你觉得这一批改动逻辑上是一件事了,再一次性冻结。这个设计把“我改了什么”和“我要提交什么”两个动作解耦了,前者是随手的、混乱的、一天几十次;后者是需要克制的、成组的、一天可能只有三五次。

我自己的习惯是:写代码的时候完全不管提交,等到一个功能点跑通了,再回头用git add -p逐块挑选,把调试用的console.log、临时注释、格式调整统统排除在外。这个流程如果没有暂存区,根本没法实现。

1.3 提交对象里存的到底是什么

这里有个流传很广的误解:很多人以为 Git 存的是“差异”。其实不是,Git 存的是快照。

一次提交(commit 对象)里装的东西非常简洁,用git cat-file -p HEAD就能看个明白:

$ git cat-file -p HEAD tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 parent 8f3a1c2... author 某开发者 <dev@example.com> 1700000000 +0800 committer 某开发者 <dev@example.com> 1700000000 +0800 fix: 修正分页参数越界

它包含四类信息:一个tree指针(指向这次提交的完整目录树)、零到多个parent(父提交,合并提交会有两个以上)、作者与提交者信息(含时间戳和时区)、以及提交说明。注意它不包含任何文件内容本身,文件内容在 blob 对象里,目录结构在 tree 对象里,commit 只是一张“入口卡片”。

这种“快照 + 内容寻址”的设计带来一个直接后果:同样的文件内容在仓库里只会存一份。你改了一千次同一个文件,只要某一版内容和历史某版完全一致,Git 就直接复用那个 blob。所以那些担心“Git 仓库会随着提交次数线性膨胀”的顾虑,在绝大多数场景下是不成立的。

1.4 这个设计的代价在哪里

快照模型的代价是首次克隆偏慢,尤其是历史很长、二进制文件很多的大仓库。因为克隆要拉取所有对象,而不是只拉最新版本。这也是为什么大文件(比如设计稿、视频素材、编译产物)放进 Git 会被反复劝退——它们每次改动都会生成一个全新的 blob,压缩率又低,仓库体积会肉眼可见地膨胀。

我的做法是:超过几 MB 的二进制文件一律走专门的存储方案,仓库里只保留引用或者生成脚本。这不是洁癖,是实打实的效率问题——一个几百 MB 的仓库,光是git status都要等一两秒,日常体验会被拖垮。

2. git add:决定哪些改动进入下一次提交

2.1 三种常用写法的区别,别再用错了

git add的写法看着多,其实核心就三个维度:范围(全仓库还是某个路径)、对象(新增/修改/删除)、以及是否交互式。

写法作用范围是否包含删除是否包含新增未跟踪文件
git add <路径>指定路径是(2.0 之后)是
git add .当前目录及子目录是(2.0 之后)是
git add -u已跟踪文件是否
git add -A全仓库是是
git add -p交互式逐块选择视选择而定否

这里有个历史遗留的坑必须说清楚:Git 2.0 之前,git add .不会记录删除操作。所以在一些老教程、老脚本里,你会看到“删文件要用git add -u”的说法。2.0 之后这个行为统一了,git add .在仓库根目录执行时基本等价于git add -A。但如果你的脚本要跑在版本不确定的环境里,用-A显式表达意图更稳妥。

还有一个细节:git add .是相对于当前目录的。你在src/utils/目录下执行,就只有这个子目录的改动进暂存区。很多人在子目录里敲了git add .,然后 commit 完发现根目录的改动没进去,反复排查半天,其实问题就出在这儿。

2.2 用 -p 做分块暂存,让一次提交只干一件事

git add -p是我认为最被低估的一个选项。它会把每个文件的改动拆成若干个 hunk(连续改动的区块),逐个问你“要不要暂存”,你按y要、n不要、s拆分更细的块、e手动编辑、q退出。

为什么值得花时间学?因为提交的可读性直接决定了以后排障的成本。想象一下半年后你git bisect定位一个 bug,结果发现历史里全是“update”“fix bug”“改了一下”这种提交,每次提交里混着三四个不相关的改动,定位过程会痛苦到想砸键盘。

举个实际场景:你在一个文件里既修了一个拼写错误,又调整了一段业务逻辑。这两件事在逻辑上完全无关,理应是两次提交。用-p模式,几秒钟就能拆开:

$ git add -p src/service.js @@ -12,7 +12,7 @@ - // 获取用户列表 + // 获取用户清单 @@ -48,9 +48,15 @@ - const page = req.query.page + const page = Math.max(1, parseInt(req.query.page, 10) || 1)

第一个 hunk 按y,第二个按n先跳过,commit 一次;再git add -p把第二个块加进去,commit 第二次。两次提交各自独立,回退、cherry-pick、review 都方便。

2.3 删除、改名与强制添加的边界情况

删除文件有两个做法:直接rm然后git add -A,或者用git rm。两者最终效果一样,区别在于git rm是原子的,删文件和暂存一次完成,中间不会出现“文件已经没了但暂存区还认为它在”的中间态。批量删文件时我更倾向后者,因为可以配合通配符:

$ git rm 'logs/*.log'

改名这件事,Git 其实不记录“改名”这个动作,它只看到“删了一个文件、加了一个文件”。你在git status里看到的renamed: a.js -> b.js是 Git 在 diff 时根据内容相似度推断出来的。默认相似度阈值是 50%,改动太大就推断不出来,会显示成一删一增。这没有对错,只是理解历史时要知道别被表象骗了。

强制添加用-f,最常见于文件已经被.gitignore拦住但又确实需要提交的情况,比如某个被忽略目录下的占位文件.gitkeep。这个操作要谨慎,一旦文件被跟踪,忽略规则就对它失效了,以后每次改动都会出现在待提交列表里。

提示:git add之后反悔,用git restore --staged <文件>(老版本是git reset HEAD <文件>)把它撤出暂存区,工作区的改动不受影响。

3. git commit 与 git status:提交动作与状态感知

3.1 commit 到底做了什么,以及 -a 参数的陷阱

git commit干的事情可以拆成四步:读取暂存区的目录树、生成 tree 对象、生成 commit 对象(带上父提交和元信息)、把当前分支指针移到新提交上。整个过程是原子的,中途失败不会留下半成品。

-a(等价于--all)是个高频参数,它等于“先对所有已跟踪文件执行一次 add,再 commit”。听起来很方便,但有两个陷阱:

第一,它对未跟踪的新文件完全无效。你新建了一个utils.js,git commit -am "add utils"之后发现文件根本没进去。这个坑我踩过不止一次,尤其是在快速迭代、频繁新建文件的阶段。

第二,它会把所有已跟踪文件的改动一股脑塞进同一个提交,直接摧毁提交的原子性。调试时留下的console.log、临时改的接口地址,全都会被带进去。

我的建议是:除非你非常确定当前工作区只有一处逻辑改动,否则别用-a。手动 add 那几秒钟,换来的是历史可读性。

顺便说一个特殊情况——空提交。有时候你需要触发一次 CI 构建,或者想在历史里打一个标记点,但工作区没有任何改动。这时候用:

$ git commit --allow-empty -m "chore: 触发重新构建"

这个提交的 tree 和父提交完全一样,但 commit 对象本身不同,所以散列值也不同,历史里会实打实多出一条记录。

3.2 提交信息怎么写到“半年后还看得懂”

提交信息的形式,社区里讨论了很多年。我自己的标准很简单:标题行说清楚做了什么,正文说清楚为什么这么做。

标题行控制在 50 个字符以内,用祈使句,别用“修改了”“添加了”这种过去式。中文语境下我喜欢加一个类型前缀,比如fix:、feat:、refactor:、docs:,不是为了教条,而是让git log --oneline一眼扫过去就能区分改动性质。

正文和标题之间空一行,正文每行折到 72 字符左右。正文里写什么?写这个改动要解决的问题是什么、为什么选这个方案而不是另一个、有没有已知的副作用。这些信息在代码里是看不出来的,半年后你自己都需要。

$ git commit -m "fix: 修正分页参数越界导致的首屏空白" -m "前端传入的 page 可能是字符串 \"0\" 或负数,原实现直接用于 slice 计算,导致返回空数组。现在统一在边界处做一次归一化,同时对非数字输入降级为 1,避免下游再重复判断。"

用两个-m会自动在中间插入空行,比进编辑器写方便。如果提交信息比较长、需要反复斟酌,那就配置好编辑器(git config core.editor),直接git commit不打-m,在编辑器里从容地写。

3.3 status 的两列字符,很多人只看了个大概

git status的默认输出比较啰嗦,加上-s(short)之后就清爽了,但那个两列字符的含义,不少人一直是模糊的。

$ git status -s MM src/app.js M src/config.js A src/new.js D src/old.js ?? temp/
  • 左列表示暂存区相对于 HEAD 的差异(也就是“这次要提交什么”)
  • 右列表示工作区相对于暂存区的差异(也就是“还没 add 的改动”)
  • ??是未跟踪文件,.gitignore里被忽略的文件根本不显示

所以MM的意思是:这个文件有一部分改动已经 add 了,add 完之后又改了一部分。A是新增并已暂存,D是工作区已删除但还没暂存这个删除。

知道这个之后,git status -s就能当仪表盘用。我习惯在每次 commit 前扫一眼,确认没有多余的??和意料之外的M。

另外git status还会提示分支的领先/落后情况,比如Your branch is ahead of 'origin/main' by 2 commits。这个信息在多人协作时非常有用——它告诉你本地有没有没推上去的提交,避免“我以为推了其实没推”的尴尬。

3.4 --amend:改掉刚刚那次提交

提交完立刻发现漏了一个文件,或者提交信息有错别字,这是常态。--amend就是为这个场景准备的:

$ git add missing-file.js $ git commit --amend --no-edit

--no-edit表示沿用原来的提交信息。如果要改信息,去掉这个参数,编辑器会打开。

这里必须强调一件事:amend 生成的是一个全新的提交对象,散列值会变。原来那个提交并没有被修改,它变成了一个没有分支指向的“悬空对象”,等着垃圾回收。所以如果这个提交已经推送到远端、并且别人基于它做了工作,amend 之后强推会造成别人的历史分叉头大。判断标准很简单:没推过随便改,推过了先问一句。

还有个小技巧,--amend也能改作者和提交时间:

$ git commit --amend --reset-author $ GIT_COMMITTER_DATE="2024-01-01 10:00:00" git commit --amend --no-edit

前者在配置了错误的 user.email 之后很管用,后者在补录历史时偶尔用到。注意author date和committer date是两个不同的字段,rebase 会保留前者、刷新后者,理解这一点对读懂git log很有帮助。

4. git stash:切分支之前的那次“临时收摊”

4.1 stash 存的到底是什么

场景很典型:功能写到一半,线上报了个紧急问题需要立刻切分支去修,但当前改动还不够一次提交。这时候git stash就是那个“先把桌上的东西扫进抽屉”的动作。

它做的事其实很朴素:把工作区和暂存区的改动打包成两个(或三个)commit 对象,挂到refs/stash这个特殊引用上,然后把工作区恢复到 HEAD 的状态。所以 stash 本质上不是某种“临时存储机制”,就是一组藏在暗处的提交。

理解了这一点,很多事情就顺了:git stash能保存的东西,和一次提交能保存的东西是一样的;它同样会产生对象、同样可以被 diff、同样占用仓库空间。

默认的git stash不包含未跟踪文件。这个限制坑过无数人——新建了几个文件,stash 之后切分支,回来发现新文件还在原地没被动过,或者更糟,被后续的操作覆盖了。要包含未跟踪文件,得显式加参数:

$ git stash push -u -m "分页改造半成品" $ git stash push -a -m "连忽略文件一起存"

-u(--include-untracked)把未跟踪文件也收进去,-a(--all)连.gitignore忽略的文件一起收。-m加备注,强烈建议加——等你攒了五六个 stash,stash@{3}这种名字根本记不住是哪个。

4.2 引用栈的结构与常用子命令

stash 是栈结构,后进的编号小。stash@{0}永远是最新的一条。

$ git stash list stash@{0}: On feature/pagination: 分页改造半成品 stash@{1}: WIP on main: 8f3a1c2 fix: 修正排序 stash@{2}: On hotfix: 临时调试代码

常用操作我整理成一张表,日常基本够用:

命令作用是否删除栈内记录
git stash保存当前改动并清空工作区-
git stash list查看栈否
git stash show -p stash@{n}查看某一条的具体差异否
git stash apply stash@{n}应用某一条否
git stash pop应用最新一条是
git stash drop stash@{n}丢弃某一条是
git stash clear清空整个栈是

日常我几乎只用apply不用pop。原因很实际:pop在应用成功的同时就把记录删了,万一应用完发现有一处冲突没处理好、想回头对比一下原始版本,就没得看了。用apply之后确认没问题,再手动drop,多一步但心里踏实。

4.3 冲突时 pop 和 apply 的行为差异

这一点值得单独拎出来说,因为它的行为差异很多人不知道。

git stash apply遇到冲突时,stash 记录保留在栈里,你需要手动解决冲突,处理完再drop。

git stash pop遇到冲突时,同样不会删除记录(这点和很多人的印象相反),但它的输出容易让人忽略冲突状态。更麻烦的是,如果冲突解决得不对,你可能已经记不清原始版本长什么样了。

所以碰到大概率会冲突的场景(比如改动涉及的文件在目标分支上已经被大改过),我的流程是:先git stash show -p stash@{0}看一眼差异,评估一下冲突范围,然后apply,手动解决,确认测试通过,最后drop。

还有一个很少人用的功能:git stash branch。它会基于 stash 创建时的那个提交新建一个分支,然后把 stash 应用上去。如果 stash 放了很久、原分支已经变了很多,这个命令能让你在一个干净的环境里处理,比在当前分支硬解冲突舒服得多:

$ git stash branch recover/pagination stash@{0}

5. 散列值与历史:读懂那串四十位字符

5.1 提交 ID 是怎么算出来的

每次提交都会生成一个四十位的十六进制字符串,这就是提交散列值(commit hash)。它不是随机数,而是对提交对象内容的哈希计算结果。

Git 计算哈希的方式是:把对象类型、内容长度、一个空字节、以及对象内容拼在一起,对整串做 SHA-1 运算。所以只要内容有一个字节不同,结果就完全不同。反过来,内容完全相同的两个对象,哈希一定相同——这就是内容寻址的含义。

这个特性带来的好处是完整性可验证。任何人只要改动了历史里的任何一个字符,从那一点往后的所有提交 ID 都会变,篡改无处可藏。这也是分布式协作能成立的基础:不需要中心节点来担保,哈希本身就是担保。

实际使用中,你不需要写全四十位。Git 允许用短哈希,只要在当前仓库里能唯一定位就行,通常七八位足够。但要注意,短哈希的“唯一”是在当前仓库范围内成立的,仓库变大了之后可能出现歧义,那时候 Git 会提示你写长一点。

5.2 父提交、分支指针与 HEAD 的关系

提交之间靠parent字段串成一张有向图(DAG),不是一条简单的链表。普通提交有一个父提交,合并提交有两个或更多,而仓库里的第一个提交没有父提交(root commit)。

分支在这个模型里轻得不可思议——它只是一个指向某个提交的文本文件,里面存着那串四十位字符。所以“创建分支”这个操作的成本几乎为零,git branch new-feature就是往.git/refs/heads/下写一个文件。

HEAD则是一个指针的指针,通常指向当前分支(比如refs/heads/main),分离头指针状态下直接指向某个提交。这也是为什么git checkout <提交ID>之后你会处于“detached HEAD”状态——HEAD 不再通过分支间接指向,而是直接指着一个提交,此时的新提交不属于任何分支,切走之后就很难再找到了(除非用 reflog)。

理解了这三者的关系,很多命令的行为就变得可预测了。git reset --soft HEAD~1是移动分支指针但不动暂存区和工作区;--mixed多动一个暂存区;--hard三个全动。它们的区别不是设计出来的规则,而是“动了哪几层”的自然结果。

5.3 log 的几种高价值组合

git log的参数多到能写一本书,但日常真正高频的就那么几个组合。

# 最常用:单行 + 图形 + 引用名,一屏看清分支结构 $ git log --oneline --graph --decorate --all -20 # 看某个文件的历史,含改名追踪 $ git log --follow -p -- src/service.js # 看某段时间内谁改了什么 $ git log --since="2 weeks ago" --stat # 搜索提交信息里的关键词 $ git log --grep="分页" --oneline # 搜索代码内容在哪次提交里被引入或删除 $ git log -S"parseInt(req.query.page" --oneline

最后那个-S参数(俗称 pickaxe)是排查问题的利器。线上某段逻辑行为不对,你想知道它是哪次提交引入的,直接-S搜那段代码,比翻历史快得多。如果要按正则匹配,用-G。

--follow也值得单独提一句。文件被改过名之后,普通git log -- 路径只能看到改名之后的历史,加上--follow才会顺着改名关系往前追。这个在排查“这段代码为什么长这样”的时候非常关键。

5.4 reflog:误操作之后的兜底入口

如果说 Git 有什么“后悔药”,那就是 reflog。

它记录的是HEAD 和分支指针的每一次移动,包括 checkout、commit、reset、rebase、merge 等等。默认保留 90 天,而且这些都是本地记录,不会被推送、不会被别人影响。

$ git reflog 8f3a1c2 HEAD@{0}: reset: moving to HEAD~3 4b825dc HEAD@{1}: commit: feat: 新增导出功能 a1b2c3d HEAD@{2}: commit: fix: 修正时间格式

有了它,误reset --hard之后找回丢失的提交就是几秒钟的事:

$ git reset --hard HEAD@{1}

或者更稳妥一点,先看一眼那个提交的内容再决定:

$ git show HEAD@{1} $ git branch rescue/feature HEAD@{1}

我个人的经验是,reset --hard之前先git branch backup/xxx打个保险,虽然 reflog 基本都能救回来,但养成这个习惯之后,操作时心态会放松很多。压力小了,反而不容易出错。

如果 reflog 也找不到了(比如超过保留期,或者对象被 gc 清理),还有最后一招git fsck --lost-found,它能扫出所有悬空对象,输出到.git/lost-found/目录。不过到这一步基本就是碰运气了,能救回来多少算多少。

6. 多次提交与忽略规则:把历史整理成能看懂的样子

6.1 提交粒度的判断标准

“一次提交应该包含多少改动”这个问题,没有标准答案,但有一个判断标准很好用:这次提交能不能用一句不带“和”字的话描述完。

如果描述里出现了“顺便”“同时”“另外还”,那大概率应该拆成两次。反过来,如果一次提交只改了一个拼写错误,也没必要为了“凑数”硬加别的东西。

我自己的拆分维度优先级是这样的:功能改动 > 缺陷修复 > 重构 > 格式调整 > 文档。这五类混在一起提交是历史可读性的头号杀手,尤其是格式调整(比如整个文件重新缩进)混进功能改动,diff 会变成一片红绿,review 的人根本看不出真实改动在哪。遇到这种,先单独提交一次格式化,工具配置里加一个.git-blame-ignore-revs把格式化提交排除掉,blame 的时候就不会被污染。

还有一个实操层面的建议:提交前跑一遍git diff --staged。这一步能拦下大量低级失误——调试代码、临时注释、误删的空行、忘了删的TODO。几十秒的成本,省下的是以后“这个改动哪来的”的困惑。

6.2 .gitignore 的匹配逻辑与几个高频误区

.gitignore是每个项目必备,但它的规则细节比想象中复杂。

基本语法不复杂:空行和#开头的行被忽略;/结尾表示只匹配目录;!开头表示取反(重新纳入跟踪);*匹配任意字符但不跨目录,**可以跨目录;行首的/表示从仓库根目录开始匹配,否则可以在任意层级匹配。

# 忽略所有 node_modules 目录 node_modules/ # 只忽略根目录下的 build /build/ # 忽略所有 .log 文件,但保留 important.log *.log !important.log # 忽略任意层级的 temp 目录下的所有内容 **/temp/

最容易踩的坑是这一条:.gitignore对已经被跟踪的文件无效。文件一旦被git add过,之后再加进忽略规则也不会生效,它会继续出现在git status里。要真正忽略它,得先把它从索引里摘出来:

$ git rm --cached config/local.env $ echo "config/local.env" >> .gitignore $ git commit -m "chore: 停止跟踪本地配置文件"

注意--cached只删索引记录,本地文件还在,别漏了这个参数,不然文件真的会被删掉。

第二个高频坑是取反规则的失效。如果父目录被忽略了,子文件就没法用!恢复:

logs/ !logs/keep.txt # 这条不生效!因为父目录已被忽略

正确写法是先放行目录,再忽略目录下的全部,最后单独放行目标文件:

logs/* !logs/keep.txt

第三个坑是忽略规则的作用域。.gitignore是分层的,子目录里的.gitignore只对该目录及以下生效,可以覆盖上层的规则。另外还有两个不常被提到的位置:.git/info/exclude用于本仓库的个人忽略(不进版本库),以及全局忽略文件(通过git config --global core.excludesFile指定),适合放编辑器生成的临时文件,比如.idea/、.vscode/、*.swp。

6.3 用 check-ignore 定位规则冲突

规则一多,某个文件为什么被忽略、或者为什么不生效,就很难靠眼睛看了。Git 提供了专门的反查命令:

$ git check-ignore -v config/local.env .gitignore:12:config/*.env config/local.env

输出会告诉你:是哪个文件的第几行、哪条规则命中了这个路径。排查“为什么这个文件加不进去”的时候,这一条命令能省掉十几分钟的猜谜。

如果要看所有被忽略的文件清单,用:

$ git status --ignored -s

这个在确认打包产物、缓存目录有没有被正确排除时很有用。

6.4 整理历史:amend 和交互式 rebase 的分工

提交不是一次成型的,很多时候需要回头整理。两个手段各有适用范围。

--amend只处理最新一次提交,适合“刚提交完就发现漏了东西”的场景,操作简单、风险低。

git rebase -i HEAD~n能处理最近 n 次提交,功能强大得多:可以合并(squash)、重排、改写信息、删除、甚至在中间插入新提交。但它会重写历史,所有被影响的提交 ID 都会变。所以同一条铁律:只在还没推送的分支上做。

$ git rebase -i HEAD~3

打开编辑器后,把想合并的提交前面的pick改成squash(保留信息合并)或fixup(丢弃信息合并),保存退出,Git 会依次应用。

我用这个功能最多的场景是:开发过程中攒了七八个“wip”“调试”“再改一下”的碎提交,功能完成之后合并成一到两个语义清晰的提交再推送。这样做不是为了好看,是为了让 code review 的人能看懂你干了什么——一堆碎提交的 diff 拼在一起,审查体验极差。

注意:rebase 过程中如果冲突,解决完用git add标记,然后git rebase --continue。中途想放弃,git rebase --abort会完全恢复到操作前的状态,这个命令可以放心用。

7. 常见问题与排查速查

下面这些是我这些年被问得最多、自己也踩过的场景,整理成表,遇到问题时可以直接对号入座。

现象大概率原因处理方式
新文件不在待提交列表被.gitignore命中git check-ignore -v <文件>定位规则
改了.gitignore但文件还显示文件已被跟踪git rm --cached <文件>后再提交
commit -am后新文件没进去-a只作用于已跟踪文件先git add新文件
stash 之后新文件还在默认不含未跟踪文件用git stash push -u
pop冲突后记录没了误以为冲突会保留(实际会保留,但容易误删)优先用apply,确认后再drop
提交后想改内容需要重写提交git add后git commit --amend --no-edit
误 reset 丢了提交分支指针被移走git reflog找到 ID,git reset --hard <ID>
短哈希提示歧义仓库变大后前缀不唯一多写几位,或用git rev-parse确认
空目录提交不上去Git 不跟踪空目录目录里放一个.gitkeep占位
提交时间显示不对时区或系统时间问题检查git config user.email和系统时区

再说几个表格装不下、但很值得单独提的细节。

第一,git commit报 “Please tell me who you are”。这是新手第一天必遇的问题,原因是没配user.name和user.email。配置分两级:--global影响所有仓库,仓库内不带参数只影响当前仓库。我的习惯是全局配一个通用身份,然后在公司项目里单独配仓库级的,避免混用。

第二,提交信息写错了但已经推送。这种情况下不建议强推改写,直接接受它。如果信息错得离谱(比如把敏感信息写进去了),那要考虑的不只是改历史,还有凭据轮换的问题。

第三,git status显示HEAD detached。说明你现在不在任何分支上,此时的新提交不属于任何分支。补救很简单:git switch -c new-branch把当前提交接到一个新分支上。如果不想要这些提交,直接git switch main切走就行。

第四,合并提交导致git log特别乱。团队里如果有人习惯git pull产生大量 merge commit,历史会变成一团麻。可以在 pull 时配置git config pull.rebase true,让拉取走变基,保持线性历史。这个配置在多人协作仓库里几乎是必选项。

第五,git stash攒了十几个忘了清理。定期git stash list扫一眼,没用的drop掉。stash 不是免费的,它占用的对象不会被自动回收,长期堆积会让仓库体积慢慢变大。

第六,git diff和git diff --staged的区别。前者是工作区 vs 暂存区,后者是暂存区 vs 最新提交。很多人 commit 前想看一眼“这次到底要提交什么”,结果敲了git diff,看到的是还没 add 的部分,白看一场。这个混淆太常见了,记住一句话:要提交什么,看--staged。

最后分享一个我用了很多年的小习惯:在.git/hooks/pre-commit里放一个轻量检查脚本,拦截console.log、debugger、合并冲突残留标记(<<<<<<<)这几类东西。脚本用最朴素的 shell 写就行,几行 grep 的事,但它拦下的失误比任何规范文档都管用。工具能防住的东西,就别指望自觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询