1. commit 的三道前置关卡身份配置、提交信息、amend 的边界1.1 username 和 email 不配置第一个提交就会撞墙很多新手装完 Git 后的第一个操作是 git init然后迫不及待地执行 git add . 和 git commit -m first commit。这个流程本身没有错但如果你只停留在 git commit 这一步后面面对 git branch、git checkout、git reset、git rebase 这些命令时很容易觉得每个命令都像带刺的荆棘。我真正觉得 Git “打通了”是从搞懂 git commit 的身份配置、提交信息规范以及 HEAD、相对引用、reset、revert、cherry-pick 这一整条线开始的。第一个劝退场景就是身份配置报错。如果你在没有任何配置的情况下执行 git commitGit 会直接拒绝并抛出一段英文提示Author identity unknown告诉你需要设置 user.name 和 user.email。我第一次遇到时还以为是安装出了问题后来才明白Git 必须在每个提交里记录“谁在什么时候做了什么”所以这两个字段是提交的强制元数据缺一不可。正确的配置方式如下git config --global user.name 你的名字 git config --global user.email 你的邮箱这里我建议认真想一想邮箱填什么。用 --global 配置的身份会应用到这台电脑的所有仓库。如果你同时维护公司和个人的项目最好别只配一个全局邮箱否则个人项目的提交记录里会全是公司邮箱日后开源或跳槽时都很尴尬。Git 的配置作用域有三层--system整台机器、--global当前用户、--local当前仓库优先级是 local global system。我的个人习惯是全局配一个通用邮箱在公司仓库目录下单独执行 git config user.email 配 local 覆盖。还有一个很少被人提起的坑为什么明明配置了提交时还是报错通常是环境变量 GIT_AUTHOR_NAME、GIT_AUTHOR_EMAIL、GIT_COMMITTER_NAME、GIT_COMMITTER_EMAIL 被设成了奇怪的值Git 会优先读取这些环境变量。排查时先跑 git config --list --show-origin看看当前生效的配置到底来自哪里基本就能定位是文件配置还是环境变量的问题。1.2 提交信息怎么写才算“能看懂的历史”提交信息看起来无足轻重实际是很多人回看历史时最痛苦的地方。一个仓库运行半年后最值钱的东西往往不是代码本身而是提交历史里的“为什么”。如果你每次提交都是 update、fix、111三个月后再看连你自己都想不起这次改动是为了什么。我推荐至少做到两点前缀统一以及带上关联单号或问题描述。git commit -m feat: 增加订单导出功能 git commit -m fix: 修复并发下单时库存扣减异常 (#1234) git commit -m docs: 更新部署说明前缀的好处是 git log --oneline 扫一眼就能看出一次提交的类型配合 CI 时前缀还可以直接决定是否触发版本号更新或 changelog 生成。关联单号则是为了检索git log --grep1234 能瞬间筛出所有相关提交debug 效率会明显提升。如果你觉得每次都写 -m 很麻烦可以配置提交模板让每次的编辑器都自动打开一份规范结构git config commit.template .gitmessage模板里写好类型、标题、正文的骨架提交时按图索骥填内容。这个习惯对团队协作的提升非常明显因为提交信息一旦规范code review 的效率也能上一个台阶。我自己带项目时最喜欢翻的提交历史永远是那些“一条信息就能讲清楚一个决策”的提交。1.3 git commit --amend忘加文件、写错信息时的后悔药git commit --amend 是很多人搜过的命令因为它解决的场景太日常提交完发现漏了一个文件或者提交信息打错了一个字。# 漏加文件想并进上一次提交 git add 漏掉的文件 git commit --amend --no-edit # 提交信息写错了 git commit --amend -m 正确的提交信息--no-edit 表示沿用原来的提交信息只更新内容。如果不用这个参数Git 会打开编辑器让你重新确认。这个组合适合“只补文件、不改消息”的场景能省掉一次手打提交信息的过程。但 --amend 有一条必须记住的边界它只能改最近一次提交而且本质上是“用新提交替换旧提交”提交的 SHA-1 会改变。如果这个提交已经推到远端又有同事基于它继续开发你 amend 之后对方的记录就会和远端对不上后面就是一连串的冲突和混乱。所以我的铁律很简单只对还没 push 出去的提交用 amend。已经进入公共分支的提交做错了就新增一个修复提交历史丑点没关系公共历史被单方面篡改才是最大的坑。2. branch、checkout、switch分支操作的前世今生2.1 branch 不是复制代码而是移动一个“书签”很多人接触分支时默认的模型是“创建分支等于复制一份代码”。如果真是这样大仓库每建一次分支都要拷贝几百 MB速度不可能快。Git 里的分支本质上只是一个指针文件指向某个 commit。创建分支只是新建一个指针所以瞬间完成。git branch # 查看本地分支当前分支前有 * 号 git branch feature/login # 创建分支只是新建一个指向当前 HEAD 的指针 git branch -d feature/login # 删除已合并的分支 git branch -D feature/login # 强制删除未合并的分支既然是“书签”它自然可以被移动。git branch -f 就是干这个的git branch -f feature/login HEAD~2这个命令把 feature/login 这个书签强制移到“当前 HEAD 往前数两步”的提交上。日常开发里我用 -f 不多但理解了它就理解了“分支是可以随时移动的引用”这个底层概念。再往后学 reset你会发现 reset 干的也是同样的事——移动分支指针只是它把工作区和暂存区的联动规则也一并处理了。删除分支时要注意 -d 和 -D 的区别-d 只允许删除已合并分支-D 是强制删除。我职业生涯里唯一一次慌乱就是手滑用 -D 删了一个还挂着重要工作的分支。那次之后我养成了习惯删除前先用 git branch --merged 看一下哪些分支真的可以安全删除操作时多用 -d不要轻易上 -D。2.2 checkout 身兼两职为什么让新手一脸懵git checkout 是 Git 早期最核心的切换命令但现在回头看它最大的问题是语义过重身兼两职。职责一切换分支或 commit。职责二把某个文件恢复到指定提交的状态。git checkout feature/login # 职责一切换分支 git checkout -b feature/new # 职责一加新建并切换 git checkout -- index.html # 职责二恢复文件到 HEAD 状态第一次用 Git 的人很容易把两个职责搞混。我见过同事想撤销某个文件的修改输入 git checkout feature/login结果文件没恢复人倒是切到了另一个分支。这件事发生在干净工作区还好如果工作区里还挂着未提交的改动切换瞬间会触发 Git 的保护机制或把改动带到另一个分支场面立刻混乱。git checkout -b xxx 这个缩写其实很常用如果你想少打几个字靠它“建分支并切换”没问题。但如果是给新手讲解我更推荐下一小节里的 switch因为它的职责单一语义就像“换一条路走”一样自然。2.3 switch 与 restoreGit 2.23 的职责拆分Git 2.23 之后官方把 checkout 的职责拆成了两个命令git switch 负责切换分支git restore 负责恢复文件。git switch feature/login # 切换分支 git switch -c feature/new # 新建并切换 git restore index.html # 恢复文件到 HEAD git restore --staged index.html # 把暂存区里的文件撤回到工作区新命令最大的改进是语义清晰。尤其是 --staged 这个选项解决了一个高频痛点git add 之后想反悔。以前要么 git reset HEAD 要么 git restore --staged 后者从字面上就能看懂。但我也要替 checkout 说句公道话它没有过时不少场景仍然要用它比如 git checkout 进入 detached HEAD 查看历史代码。我给团队的建议是新操作习惯、脚本、文档全部往 switch/restore 上迁遇到旧脚本里的 checkout 不用紧张关键是搞清楚每次命令动的是 HEAD 指针、分支指针还是某个文件的内容。3. HEAD 与相对引用在提交历史里精确“指路”3.1 HEAD 只是一个指针detached HEAD 是怎么回事HEAD 的中文翻译“头指针”很形象但很多人仍把它理解成“当前代码的副本”。准确的说法是HEAD 是一个指针指向当前所在的分支分支再指向某个 commit。绝大多数情况下你执行 git log 看到的顶端提交就是 HEAD 的最终落点。HEAD - main - commit C什么时候会出现 detached HEAD当你执行 git checkout 直接切换到某个提交时HEAD 就不再指向分支而是直接挂在那个提交上。这个状态下你继续提交新提交会挂在它下面但不属于任何分支等你切回正常分支后这些提交就像消失了一样。我第一次踩这个坑是查一个历史 bug 时顺手 checkout 了一个旧版本做了两个验证用的小提交切回主分支后怎么都找不到还以为 Git 坏了。正确的补救方法是如果 detached HEAD 状态下做了有价值的提交立刻 git switch -c 新分支名把这些悬空提交认领到一个分支上否则它们迟早会被垃圾回收。3.2 ^ 与 ~ 的计算规则什么时候用哪个相对引用是 Git 提供的“走回头路”的快捷方式。~ 和 ^ 长得像作用并不完全一样。HEAD~ # 等价于 HEAD^取第一个父提交 HEAD~2 # 沿第一父级链向上走两步 HEAD^2 # 取第二个父提交只对 merge 提交有意义在非 merge 提交上~ 和 ^ 的结果几乎一样所以很多教程混着写。到了 merge 提交区别就出来了merge 提交有多个父提交~ 永远沿着主线的第一父级走^n 可以精确指定第 n 个父提交。我用一张表总结最常见的写法写法含义HEAD~ / HEAD^当前提交的上一个提交第一父级HEAD~2沿主线向上走两步HEAD^2merge 提交的第二个父提交main~3从 main 所在提交沿主线向上数第三步日常开发里 HEAD~2、HEAD~3 这种写法几乎每天用到。唯一要注意的是如果历史里混着 merge 提交~n 的结果可能和你的直觉有偏差因为 ~ 永远走主线不会跳到被合并进来的那条线。需要跳线时才用 ^。3.3 相对引用的三种实战用法相对引用不是独立命令而是一种“地址语法”可以塞进任何接受提交参数的命令里。列三个我日常最常用的场景。场景一查看最近几个提交分别改了什么。git show HEAD~2 git log -p HEAD~3场景二做范围对比比如“上一个提交到当前提交之间发生了什么”。git diff HEAD~1 HEAD git log main..feature场景三配合 reset 和 rebase 做批量操作。git reset --hard HEAD~3 git rebase -i HEAD~3我的经验是只要是需要“在当前提交基础上往前数 n 步”的地方一律用 HEAD~n不要复制那串 40 位的 SHA-1。可读性高、不容易复制错。唯一要小心的是数错步数执行任何破坏性命令之前先 git log --oneline -n 5 确认一下目标位置到底是不是你想去的那一个。4. rebase整理提交历史的利器也是冲突高发区4.1 merge vs rebase两种合并哲学怎么选这是 Git 世界最经典的一道主观题。merge 的核心思想是“尊重历史分叉”它会把两条开发线的交汇点记录成一个 merge commitrebase 的核心思想是“重演提交”把当前分支的提交摘下来依次应用在目标分支的最新提交之后最终历史看起来是一条直线。# 用 merge 把 main 合进 feature git switch feature git merge main # 用 rebase 把 feature 变基到 main 之上 git switch feature git rebase mainmerge 的问题在于频繁同步会在历史里留下大量无意义的 merge 噪音时间一长git log --graph 像一团毛线。rebase 的问题在于它会改写提交的 SHA-1如果对共享分支乱用会让所有人的本地历史一下子面目全非。我的选择逻辑很固定自己还没推出去的本地功能分支优先 rebase保持历史线性review 时只看一串有意义的提交。已经推送到远端、有同事在拉取的公共分支只 merge绝不 rebase。团队里拉取最新主干约定 git pull --rebase避免本地平白无故出现合并提交。最后再强调一遍那条红线绝不要 rebase 一个已经被推送到远端且被共享的分支。这条规则一旦破了经常会出现“别人 pull 到一堆陌生提交、后续 cherry-pick 和 revert 全部错位”的连锁反应修复成本极高。4.2 交互式 rebasesquash、reword、drop 与 abort 安全网交互式 rebase 是整理提交历史的利器也是最容易把人绕晕的一个入口。形式上很简单git rebase -i HEAD~3执行后 Git 会打开编辑器列出最近三个提交每行开头默认是 pick。你可以改成其他指令常用几个如下指令缩写作用pickp保留这个提交不修改rewordr保留提交内容修改提交信息edite停下来手动修改或拆分提交squashs合并到上一个提交dropd删除该提交我日常用得最多的是 squash。开发登录功能时我常常分三次提交第一版框架、补上校验、清理调试代码。合入主干前执行 git rebase -i HEAD~3把第二个和第三个改成 squash第一个保持 pick最终得到一条干净的单提交记录。squash 时编辑器会汇总多个提交信息让你重新整理我通常只保留最有价值的那一句。如果中途发现改乱了最重要的安全命令是 git rebase --abort它会把你完全带回 rebase 开始前的状态一点不剩。整理历史这种操作我最怕的不是出错而是错了还要硬着头皮继续最后把仓库搞成一团浆糊。记住abort 永远能用放轻松。4.3 git pull --rebase --autostash工作区安全与冲突处理很多团队已经约定统一用 git pull --rebase因为默认 pull 的行为是 fetch 加 merge频繁使用会在本地制造大量无意义的 merge commit。加了 --rebase 之后本地未推送的提交会被“搬到”远端最新提交之上历史保持线性。但 --rebase 有一个让人烦躁的限制如果工作区有未提交的改动它会拒绝执行。这时加 --autostash 就能解决git pull --rebase --autostash这个命令会自动把本地改动 stash 起来rebase 完成后自动恢复。我长期使用下来非常稳定。唯一要注意的是自动恢复时如果恰好和远端更新改到同一个文件冲突还是会弹出来。处理顺序是先用 git status 看冲突文件手动改完后 git add再 git rebase --continue。如果 autostash 在 rebase 完成后恢复失败不要慌先 git stash list 确认暂存包还在再用 git stash pop 手动应用。我见过最危险的操作是在慌乱中使用 git stash clear那会把所有 stash 记录清空找都找不回来。5. reset、revert、cherry-pick撤销与复用的完整方案5.1 reset 的三种模式与“后悔备份”习惯git reset 是所有撤销命令里最让人紧张的一个因为它可以直接丢东西。它的本质是把当前分支指针移动到某个提交并选择性地处理暂存区和工作区。git reset --soft HEAD~1 # 指针回退工作区、暂存区都保留 git reset --mixed HEAD~1 # 默认模式工作区保留暂存区清空 git reset --hard HEAD~1 # 指针、工作区、暂存区全部回退改动消失三种模式的应用场景--soft 适合“提交完了想回退并重新拆分成多个提交”的情况代码全都还在只是提交记录变了--mixed 是最安全的默认操作常用于 git add 错了想撤下暂存--hard 则用于本地代码改得一团糟、想彻底回到某个干净状态这也是唯一会真正丢未提交改动的模式。关于 --hard我的安全习惯是执行前先留一条后路git stash # 或者干脆临时提交一次 git commit -m WIP: 临时备份因为 --hard 丢掉的未提交改动没有回收站。备份的几秒钟大部分时候派不上用场但只要你的工作值钱这一步就是保命符。另外一个相对冷门但很实用的组合是 git reset 它只把某个文件从暂存区撤回来不碰其他任何内容。这就是“不小心 add 了不该提交的文件”的标准解法比 git reset HEAD 更精准。5.2 revert为什么说它是“更文明”的回滚reset 是往后走把指针拨回去revert 是往前走保留所有历史创建一个反向提交来抵消目标提交的改动。git revert HEAD # 撤销最近一次提交 git revert commit # 撤销任意一个历史提交revert 最适合的场景是提交已经推到远端并且被别人拉取过。这时如果 reset 再强推所有协作者的历史都会分叉场面很难收拾。而 revert 生成的新提交推上去后大家 pull 下来代码内容回到了目标提交之前但历史依然是连续的不会影响任何人的已有工作。revert 的痛点是冲突。如果被 revert 的提交之后又有别的提交动过同一区域反向合并就会冲突。处理思路上你不是在加新逻辑而是在把旧逻辑的痕迹抹掉心态上要“反过来”。我第一次做时总觉得哪里不对做了几次才习惯。还有一个罕见但必须知道的坑revert 一个 merge 提交时Git 会要求加 -m 参数指定保留哪个父提交的改动否则它无法确定你要退到哪一边。遇到这种操作建议先查文档再动手不要临时发挥。5.3 cherry-pick精准摘取单个提交cherry-pick 解决的是跨分支场景里最常见的一种需求我不想合并整个分支我只想要那个分支里的某一个提交。git cherry-pick commit在多版本维护的项目里这个命令几乎是日常操作。比如线上版本发现 bug修复提交落在主干 main 上需要同步到正在维护的 release 分支但 release 分支还不能直接合并整个 main这时一行命令就够git switch release-1.0 git cherry-pick bugfix 提交的 SHA-1cherry-pick 同样会冲突处理套路和 rebase 一模一样改文件、git add、git cherry-pick --continue想退出就 git cherry-pick --abort。如果希望追溯来源加 -x 参数提交信息里会自动带上 cherry-picked from 一行审计时很有用。我的经验是cherry-pick 前先确认目标提交还没有应用到目标分支否则可能重复改动同一处代码如果一半发现选错了提交--abort 随时可以终止。这个命令看起来简单却是多分支协作里最省时间的工具之一。6. 一套连贯的真实场景把命令串起来用6.1 从 main 建功能分支、对接主干并合并的完整流程单独记命令容易忘我建议把命令挂在“一个功能分支的完整生命周期”上记。下面的流程我几乎每天都在走# 先保证本地 main 是最新的 git switch main git pull --rebase --autostash # 拉一个干净的功能分支 git switch -c feature/login # 开发中正常提交 git add . git commit -m feat: 增加登录表单校验 # 开发完成把主干推进的更新拿回来 git switch feature/login git pull --rebase origin main # 整理本地提交历史 git rebase -i HEAD~3 # 合入主干保留合并节点 git switch main git merge --no-ff feature/login每一步都有它的目的先在 main 上 pull --rebase是为了保证功能分支从一个最新的基线出发避免后面 rebase 时出现大量无谓冲突开发中使用小步提交是为了让历史可追踪合入前再 rebase 一次是为了让合并那一下的 diff 足够干净最后 merge --no-ff则是让主干上出现一个明确的合并节点以后想整体回滚这个功能只要针对这个合并节点操作就行。团队如果约定“分支开发、主干合入”--no-ff 基本是标配。如果完全不需要保留合并节点也可以用快进合并但那样主干上不会留下任何功能边界回滚和回溯的成本都会变高。6.2 提交到了错误分支用 cherry-pick 加 reset 把提交“搬”走还有一种常见事故在 main 上开发结果手一滑把本应属于功能分支的提交提交到了 main。修复思路分两步先搬走再回退# 第一步在正确的功能分支上摘取那个提交 git switch feature/login git cherry-pick 那个误提交的 SHA-1 # 第二步把 main 退回去一个提交 git switch main git reset --hard HEAD~1这个组合的关键在于cherry-pick 复制了一份提交到正确分支reset 又让 main 回到错误提交之前的状态。全程没有丢失任何代码只是做了一次“搬运”。以后遇到“提交错分支”“想在两个分支之间同步单个修复”这类需求这套思路可以直接套用。这里有一个重要的前提误提交必须还没有被推送到远端。如果已经 push 出去而且远端是公共分支就不能 reset 再强推了而是要在 main 上执行 git revert 生成一个反向提交再到正确分支执行 git cherry-pick。逻辑上仍然是“先搬走、再回退”但回退手段要换成 revert 这种不改写历史的方案。这两个命令配合使用几乎能覆盖日常开发里九成以上的“回滚、搬移、同步”场景。
阅读完成 · 觉得有帮助?