提交这件事看着简单实际上是 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 某开发者 devexample.com 1700000000 0800 committer 某开发者 devexample.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.jsgit 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的默认输出比较啰嗦加上-sshort之后就清爽了但那个两列字符的含义不少人一直是模糊的。$ 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_DATE2024-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加备注强烈建议加——等你攒了五六个 stashstash{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 --since2 weeks ago --stat # 搜索提交信息里的关键词 $ git log --grep分页 --oneline # 搜索代码内容在哪次提交里被引入或删除 $ git log -SparseInt(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 -upop冲突后记录没了误以为冲突会保留实际会保留但容易误删优先用apply确认后再drop提交后想改内容需要重写提交git add后git commit --amend --no-edit误 reset 丢了提交分支指针被移走git reflog找到 IDgit 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 的事但它拦下的失误比任何规范文档都管用。工具能防住的东西就别指望自觉。
阅读完成 · 觉得有帮助?