Git 基础命令与核心概念这八个字很多人以为自己早就跨过去了。git add .、git commit -m update、git push这套三连击闭着眼都能敲出来日常开发也确实没出过什么岔子。可一旦线上出事故需要回滚或者合并时蹦出一屏冲突标记那套熟练动作立刻失效——说不清reset --hard究竟吃掉了哪些东西讲不明revert为什么在公共分支上更稳妥甚至分不出fetch和pull差的到底是哪一步。问题从来不在于命令背不下来而在于命令背后那几个抽象层没有真正建立起来版本库里存的是什么、暂存区凭什么存在、HEAD 到底指向哪儿、撤销操作各自打的是哪个靶子。把这些想清楚之后绝大多数 Git 命令都不需要死记因为你能从模型里推导出来该用哪一条。这篇内容面向的是已经能日常提交、但一遇异常就手忙脚乱的开发者也适合那些被各种救命 command清单喂养过、却始终拼不出完整图景的人。下面这套拆解顺序是从底层模型往上走的每一步都尽量给出可复现的命令和判断依据。1. 版本库里到底存了什么快照链、暂存区与三个指针1.1 从逐行记录改动切换到整体拍快照如果带着旧式版本控制工具的思维来理解 Git第一道坎就卡在这里。传统工具的思路是记录差异第 1 版到第 2 版加了哪几行、删了哪几行回退时把差异反向应用一遍。Git 完全不是这么干的它每次提交存的是整个项目在那一刻的完整快照——所有目录结构、所有文件内容一个不落地拍一张全景照。听到每次都存全量第一反应通常是那仓库不得爆炸。这里的关键在于内容寻址文件内容先经过哈希计算得到一个唯一标识内容相同的文件只会存一份。你在一个有一千个文件的仓库里只改了一个文件再提交新快照里另外九百九十九个文件只是复用旧对象的引用真正新增的只有改动的那一个。所以快照链在存储上是极其节省的这也是 Git 切换分支能快到几乎没有延迟的底层原因——切分支只是换一个指针指向工作区文件按需回填不需要逐个文件做差异合并。理解这一点会直接影响你对很多命令的预期。比如git log看到的是提交链每一条提交都完整描述了一个可运行的项目状态而不是相对于上一版做了什么修改。这解释了为什么git checkout 某个旧提交之后你的工作区会瞬间变成那个时间点的样子而不是把后续改动一层层撤销掉。也解释了为什么 Git 的合并能在没有共同祖先的极端情况下依然工作——它比的是两份快照的内容不是补丁序列。底层对象有四类blob存文件内容tree存目录结构和文件名到 blob 的映射commit存一次快照的元信息作者、时间、提交信息、指向的 tree、父提交tag是给某个对象打的固定标签。你在.git/objects目录里看到的那堆两位十六进制命名的文件夹就是这些对象的存放位置。日常几乎不需要直接操作它们但知道这四层结构后面理解分支只是一个指向 commit 的可移动指针就不会有任何障碍。1.2 暂存区凭什么存在它不是多余的中间层初学者最常问的一个问题是为什么不能直接提交非得先add一下这不是设计上的冗余暂存区index解决的是提交粒度这个真实痛点。你为了修一个 bug 改了两个文件顺手又格式化了第三个文件还调试性地加了几行打印。这时候如果只能整体提交这一坨东西会混成一个语义混乱的提交日后排查问题、cherry-pick 或者回滚都会很痛苦。暂存区让你可以把改动拆开提交。git add -p会逐块询问你这块要不要进暂存区按y/n/s决定s还能把一个大块拆成更小的块。我见过太多人用了几年的 Git 都不知道-p这个参数实际上它是把提交历史整理干净最有效的手段之一。配上git diff看工作区和暂存区的差异和git diff --staged看暂存区和最后一次提交的差异你就能精确掌控这次要提交什么。还有一个容易被忽略的细节暂存区里存的是文件内容的快照不是待提交的文件列表。git add之后再修改这个文件工作区和暂存区就出现了差异提交进去的是add那一刻的内容不是最新的内容。这个行为坑过不少人——改完文件add了又顺手改了两行直接commit结果发现那两行没进仓库。判断方法很简单提交前跑一次git status看看有没有Changes not staged for commit或者git diff --staged确认一遍内容。1.3 HEAD、分支、标签三个指针各管一摊在最常见的正常状态下HEAD 指向当前分支分支指向最新提交标签指向某个固定提交。三者的分工是HEAD 是我现在在哪分支是这条线的末端在哪标签是这个点永久标记为某个版本。分支的特殊之处在于它会随着新提交自动前移。你在main上提交一次main这个指针就自动挪到新提交上不需要手动操作。标签则完全不动v1.0.0打完就永远指向那次提交后续无论提交多少次它都不变。这个差异决定了它们的用途分支用来推进工作标签用来锚定发布版本。HEAD 脱离分支的状态就是所谓的 detached HEAD。执行git checkout 某个提交哈希或者git checkout v1.0.0之后HEAD 直接指向提交而不是分支此时git status会明确提示HEAD detached at xxx。在这个状态下提交是允许的但新提交不属于任何分支一旦切走就很容易找不到。真要找回来也不是没办法reflog里还留着记录这个后面会讲。不过更稳妥的做法是意识到自己进入了这个状态后立刻决定要么git switch -c 临时分支名把它固定下来要么git switch -退回原分支。2. 起手式与第一次提交配置、忽略规则与提交粒度2.1 init 与 clone 的分工别搞混从零开始建仓库用git init从已有仓库拉一份下来用git clone。这两个命令的行为差异比名字看起来大得多。git init建出来的是一个只有.git目录的空仓库没有提交、没有分支名严格说 HEAD 指向一个尚未诞生的分支也没有远程地址。git clone则会完整复制远程仓库的所有历史、所有分支引用自动设置origin远程别名默认检出主分支并且把本地分支和远程跟踪分支的对应关系配好。有个不太直观的点git init之后第一次提交之前git branch是没有任何输出的git status会告诉你当前在某个分支上但no commits yet。这时候分支其实还不存在只是一个符号引用。第一次commit之后分支才真正诞生并指向那次提交。这个细节决定了为什么有些配置脚本在空仓库里执行会报错。clone还有几个常用参数值得记住。--depth 1做浅克隆只拉最后一次提交适合 CI 环境或者只想看代码不想看历史的情况代价是后续log、blame会受限有些操作需要先--unshallow补全历史。--branch 名称可以指定克隆时检出的分支避免拉下来再切。--recurse-submodules用于仓库带子模块的情况不加这个参数子模块目录会是空的。2.2 config 的四个作用域优先级别记反Git 的配置有四个层级作用范围从小到大依次是--local当前仓库存在.git/config、--global当前用户存在用户主目录下、--system整台机器、--worktree某个工作树。优先级是就近覆盖local 会盖掉 globalglobal 盖掉 system。新机器上必配的两项是用户名和邮箱它们会写进每一次提交的元信息里git config --global user.name 你的名字 git config --global user.email youexample.com如果某个仓库需要不同的身份比如工作项目和个人项目分开进到那个仓库里用--local再配一遍即可会覆盖全局设置。想看某个配置项到底是从哪个文件读来的用git config --list --show-origin输出里会带上文件路径排查为什么我的配置不生效时这条命令非常省时间。还有几个高频配置值得一次配好。core.autocrlf在跨平台协作时处理换行符Windows 上建议设成trueLinux/macOS 设成input否则整个文件可能因为换行符差异显示为全量修改。pull.rebase决定git pull默认用合并还是变基新版本 Git 在不配置这一项时会给出警告明确设成false或true可以消掉。init.defaultBranch用来指定git init时的默认分支名。alias可以给长命令起短名比如git config --global alias.st status -sb之后git st就能用这类小优化在每天敲几十次命令的场景下收益很明显。2.3 add 的用法与 .gitignore 的生效边界git add的常见形态有这么几种git add 文件名加单个文件git add 目录加整个目录下的变更git add .加当前目录及子目录下的所有变更git add -A加整个仓库的所有变更包括新增、修改、删除。git add .和git add -A在仓库根目录下执行时效果基本一致但在子目录下执行就有差别前者只管当前路径后者管全仓库。.gitignore有一道很多人踩过的坎它只对未被跟踪的文件生效。如果某个文件已经被提交进仓库之后才往.gitignore里加规则那条规则对这个文件完全无效Git 依然会追踪它的改动。正确做法是先把它从索引里移除git rm --cached 文件路径 git commit -m chore: 停止跟踪本地配置文件文件本身留在工作区只是不再被跟踪。这一步之后.gitignore才会真正起作用。忽略规则的写法也有坑。build/匹配任意层级的 build 目录/build/只匹配仓库根目录下的 build 目录*.log匹配所有日志文件!important.log是反向排除。匹配顺序是从上往下后面的规则覆盖前面的。另一个常见的坑是规则写得过于宽泛导致该提交的文件被忽略排查用git check-ignore -v 文件路径它会告诉你这个文件是被哪一条规则、在哪个文件的第几行忽略掉的。2.4 提交信息和提交粒度决定了历史好不好用提交信息这件事短期看无所谓半年后看就是灾难。update、fix bug、修改这类信息在git log里连成一排的时候等于什么都没说。我自己的习惯是用一个前缀标明提交类型后面接一句具体做了什么、为什么这么做fix: 修复订单金额在并发下重复累加的问题 原逻辑在事务外做余额校验高并发时会读到旧值。 改为在事务内使用行锁校验并补充了并发场景的测试用例。第一行控制在 50 字符内方便--oneline模式完整显示空一行写正文解释动机而不是重复代码。这不是为了好看是为了让git log --grep、git blame、出问题时二分查找定位git bisect都能真正派上用场。历史上那些含糊其辞的提交在排查线上问题时一个都帮不上忙。提交粒度同理。一个提交只做一件事这个原则听起来简单执行起来需要刻意练习。判断标准是这个提交能不能用一句话描述完而且这句话不需要出现顺便还有以及这类连接词。做到这一点之后回滚、cherry-pick、review 都会轻松很多。实在改混了还能靠git add -p拆分或者提交后用git reset --soft HEAD~1把这些改动退回暂存区重新组织。3. 分支模型的建立合并、变基与冲突处理3.1 branch、checkout、switch 的演进与选用git branch负责管理分支的创建与查看。git branch列出本地分支git branch -a连远程跟踪分支一起列git branch -d 名称删除已合并的分支-D强制删除有未合并改动时用需要谨慎git branch -m 旧名 新名重命名。切换分支早期统一用git checkout这个命令承担了太多职责切分支、恢复文件、创建分支、切到某个提交语义非常混杂。较新版本的 Git 把它拆成了两个更明确的命令git switch专门管分支切换git restore专门管文件恢复。git switch 分支名切到已有分支git switch -c 新分支创建并切换git switch -回到上一个分支。这套新命令的好处是意图清晰看命令就知道在干什么不会出现我本来想恢复文件结果切了分支这种事故。远程分支拉下来之后本地并不会自动出现同名分支。git switch 分支名在本地不存在但远程存在同名分支时会自动建立跟踪关系并检出这是新版本的行为。老版本需要git checkout -b 分支名 origin/分支名或者git switch -c 分支名 --track origin/分支名显式指定。3.2 merge 的三种结局快进、三方合并、冲突合并的结果只有三种认识清楚之后就不会对输出信息感到困惑。第一种是快进合并fast-forward。当目标分支自始至终没有分叉也就是说当前分支的所有提交都是目标分支的祖先合并时只需要把指针往前挪不需要产生新的提交。这时候git merge的输出会写 Fast-forward历史是一条直线。第二种是三方合并。两个分支各自有独立提交产生了分叉。Git 会找到两者的共同祖先用祖先、当前分支末端、目标分支末端三份快照做比对生成一个合并提交这个提交有两个父提交。输出里会出现 Merge made by the ort strategy 之类的字样。第三种是有冲突合并中断需要人工介入。这时候git status会把冲突文件列在 Unmerged paths 下面文件内容里会出现、、三组标记。快进合并虽然历史干净但会丢失这里曾经存在过一个特性分支的信息。想保留分支拓扑用git merge --no-ff 分支名它会强制产生一个合并提交。团队里对历史形态有要求的话这一项最好提前约定好避免同一个仓库里一半是直线一半是分叉。3.3 rebase 改写历史的条件与代价变基的作用是把当前分支上的提交搬到另一个基点上重放。执行git rebase main的时候Git 会找到当前分支和 main 的共同祖先把祖先之后当前分支上的提交逐个摘下来暂存到一边把当前分支重置到 main 的最新提交再把这些提交一个个重新应用上去。结果是历史变成一条直线看起来像所有工作都是在 main 最新代码之上顺序完成的。这个操作最大的价值是历史整洁最大的代价是它改写了提交哈希。重放出来的提交虽然内容和信息一样但因为父提交变了哈希必然变化。所以有一条铁律已经推送到公共分支、可能有别人基于它工作的提交不要 rebase。你改写之后强推上去别人本地的历史就和远程对不上了他们下次拉取时会产生一堆莫名其妙的重复提交。私有分支、还没推上去的本地提交随便 rebase 都没问题。交互式变基git rebase -i HEAD~3是整理本地提交的利器。它会打开编辑器列出最近三次提交每条前面有个指令pick保留reword改提交信息edit停下来修改内容squash合并到前一个提交并保留两条信息fixup合并并丢弃当前这条的信息drop删除。把一串修 bug再修一下真的修好了整理成一个语义完整的提交就靠这个。操作过程中如果搞砸了git rebase --abort可以完整退回到变基开始前的状态不用担心。3.4 冲突处理的完整链路冲突的本质是两个分支修改了同一文件的同一区域Git 无法判断该保留哪个。它不会瞎猜而是把两份内容都塞进文件里让你决定。处理流程可以固定成这几步。先用git status确认哪些文件冲突读一遍自己改了什么、对方改了什么。打开冲突文件会看到这样的结构 HEAD 当前分支的版本 目标分支的版本 feature/xxx手工把三段标记和不要的内容删掉只留下最终想要的结果。改完之后git add 文件标记为已解决所有冲突文件都 add 完git commit收尾合并场景下直接git commit会带上预备好的合并信息rebase 场景下用git rebase --continue。两个提效技巧。一是git checkout --ours 文件/git checkout --theirs 文件可以整份选择一边的版本适合那种这个文件我全要对方的场景。二是配置git config --global merge.conflictStyle zdiff3冲突标记里会额外显示共同祖先的原始内容能让你一眼看出双方各自改了什么比默认的两段式好判断得多。冲突处理中途想放弃git merge --abort或git rebase --abort回到干净状态。4. 撤销的四个靶子reset、restore、revert、reflog4.1 先定位改动现在处在哪个区撤销类命令之所以让人发怵是因为大部分人没有先判断我要撤销的东西现在在哪。Git 里改动会经过三个区域工作区你正在编辑的文件、暂存区add过的内容、版本库commit过的内容。不同的撤销需求对应不同的命令选错了轻则白干重则丢数据。一个简单的判断方法git status的输出已经帮你分好类了。Changes to be committed 是暂存区里的Changes not staged for commit 是工作区里的未跟踪文件单独列在 Untracked files。看清自己要去掉的是哪一类再去选命令比背救命清单可靠得多。4.2 reset 的三种模式区别只在动哪几个区git reset是移动分支指针的命令三种模式的区别在于它顺手把暂存区和工作区处理到什么程度。模式分支指针暂存区工作区典型用途--soft移动保留保留撤销提交但保留改动重新组织后再次提交--mixed默认移动重置保留撤销提交和暂存改动留在工作区重新挑--hard移动重置重置彻底丢弃回到目标提交的干净状态--hard是唯一会真正丢数据的模式被它覆盖掉的工作区改动没有任何恢复途径除非之前 add 过那就还能靠 reflog 找回暂存过的内容。执行之前务必用git status确认没有想保留的未提交改动或者先git stash存一下。回到上一提交的写法是git reset --soft HEAD~1HEAD~1表示当前提交的第一个父提交。要回退多个提交就写HEAD~3。想回到某个具体提交用哈希git reset --mixed abc1234。注意 reset 之后那些被甩掉的提交并没有立刻消失它们还在 reflog 里这是后面要讲的一道保险。4.3 restore 与 checkout 的分工单纯的撤销某个文件的改动不需要动用 resetgit restore更对路。git restore 文件用暂存区的内容覆盖工作区等于丢弃这个文件未暂存的修改。git restore --staged 文件把文件从暂存区撤回工作区add过的内容不丢只是不再准备提交这条命令等价于老版本的git reset HEAD 文件。git restore --source某个提交 文件可以从任意提交里取出某个文件的版本用于这个文件我要恢复到三天前的样子。这个操作只影响工作区改完还得自己决定要不要提交。老版本没有restore的时候这些活儿都靠git checkout -- 文件语义上容易和切分支混淆。如果手上的 Git 版本够新2.23 之后建议把习惯切到restore和switch命令意图更清晰误操作概率明显下降。4.4 revert 在共享分支上的不可替代性reset 的原理是把指针往回挪让历史里那些提交变得不可达。这在本地随便玩没问题但对已经推送的公共分支就是灾难——你本地历史变短了远程还是原样下次推送要么被拒要么得强推而强推会让所有基于这些提交工作的同事陷入混乱。git revert 提交哈希走的是另一条路它不删除任何历史而是生成一个新的提交内容正好是那次提交的反向操作。原来的提交还在历史里新提交把它的影响抵消掉。历史只增不减所有人都能正常拉取这是公共分支上的标准做法。有个实际场景值得一说一次合并提交merge commit之后发现整个特性分支的方向错了想撤销这次合并。直接git revert 合并提交会报错说需要指定主线因为合并提交有两个父提交Git 不知道该往哪边回。正确写法是git revert -m 1 合并提交-m 1表示以第一个父提交通常是合并时所在的那个分支为主线。这里有个后续坑被 revert 掉的分支内容如果之后想再合进来Git 会认为这些提交已经处理过而拒绝合并需要先 revert 掉那个 revert 提交才能恢复。4.5 reflog最后一道保险git reflog记录的是本地 HEAD 的每一次移动包括提交、切分支、reset、rebase、merge。这个记录默认保留 90 天可达对象到 30 天不可达对象只要提交对象还没被垃圾回收就能找回来。误操作git reset --hard把提交甩掉了解决路径是这样的先git reflog看一眼最近的记录找到被甩掉那次提交的哈希reflog 里显示的是HEAD{n}和对应的简写哈希、提交信息然后git reset --hard HEAD{3}或者git reset --hard 哈希回去。整个过程不超过半分钟。reflog只存在于本地不会被推送到远程也不会跟着 clone 走。所以它是本地误操作的保险不是远程灾难的保险。养成一个习惯做任何可能改历史的操作rebase、reset --hard、filter 类操作之前先记一下当前哈希git rev-parse HEAD的输出复制一份放到随手能拿到的地方出问题直接 reset 回去比翻 reflog 还快。5. 远程协作的边界fetch、pull、push 与跟踪分支5.1 origin 只是个别名跟踪分支才是关键origin不是什么特殊关键字它只是clone时给远程地址起的默认别名改叫别的名字完全没问题git remote rename origin upstream之后所有命令里的 origin 换成 upstream 即可。git remote -v能看到当前配置的所有远程别名和对应地址。真正决定本地分支和远程如何对应的是跟踪关系。git branch -vv会列出每个本地分支跟踪的远程分支以及领先/落后多少个提交。建立跟踪关系的命令是git branch --set-upstream-toorigin/远程分支 本地分支或者在推送时用git push -u origin 分支名一次性设好。这个关系不只是方便它直接影响git pull和git push不带参数时的行为也影响git status里那句Your branch is ahead of origin/main by 2 commits的提示。很多人觉得 Git 的远程操作不直观十有八九是跟踪关系没理清楚。5.2 fetch 与 pull 的差别到底在哪git fetch做的事是把远程仓库的最新对象和引用下载下来更新本地的远程跟踪分支也就是origin/main这类引用但不动你的本地分支也不动工作区。执行完之后你可以在本地安全地查看远程有什么新东西比如git log HEAD..origin/main --oneline看看到底多了哪些提交git diff HEAD origin/main看看改了哪些文件。git pull等于git fetch加一次合并或变基。默认行为是合并git merge origin/main配置了pull.rebasetrue就是变基。合并可能产生一个合并提交变基则会把你的本地提交重放到远程最新提交之上。这两者的差别在协作紧张的时候特别明显。直接pull遇到分叉会产生一个Merge branch main of ...的自动合并提交这种提交在历史里毫无信息量。而先fetch看一眼再决定合并还是变基你能控制历史长什么样。我自己的默认习惯是git pull --rebase本地零散提交被整齐地放到远程提交之后不会产生额外的合并节点。代价是遇到冲突时解决方式和 merge 略有不同需要git rebase --continue而不是git commit。git fetch --prune值得加进日常习惯。远程分支被删除之后本地的origin/xxx引用不会自动消失日积月累会攒下一堆已经没用的引用。--prune会在拉取时清掉那些远程已经不存在的跟踪分支。配上git config --global fetch.prune true就不用每次手敲了。5.3 push 被拒的三种典型原因推送被拒最常见的原因是远程有你本地没有的提交报错信息类似! [rejected] main - main (fetch first)。这说明别人先推了东西你的本地历史不是远程的延续。处理方式有两种先git pull --rebase把远程提交接上再推或者git pull合并后再推。直接把别人的提交强推覆盖掉是绝对不能做的。第二种是权限或者保护规则导致被拒报错会明确提到 protected branch 或者 permission denied。这种不是命令能解决的得看仓库的规则配置。第三种是推送的分支名和远程不匹配或者第一次推送新分支时没建立跟踪关系。git push -u origin 分支名一次解决。关于强推git push --force是危险的它无条件覆盖远程。如果确实需要强推比如刚 rebase 完自己的私有分支用git push --force-with-lease。这个参数会先检查远程分支的当前值是不是和你本地记录的一致如果不一致说明有人在你之后推过东西操作会被拒绝。多这一层检查能挡掉相当一部分误覆盖别人的事故。5.4 误提交大文件之后的补救路径把几 MB 的二进制文件、编译产物、甚至数据库导出文件提交进仓库是新手阶段几乎必踩的坑。麻烦在于 Git 的历史是不可变的删掉文件再提交一次历史里那个大对象依然存在仓库体积一点没减。如果只是刚提交还没推送git reset --soft HEAD~1撤销提交把文件加进.gitignore重新提交即可。如果已经推送了情况就复杂了需要重写历史。现在推荐的方案是git filter-repo这个工具需要单独安装它能高效地从所有历史中彻底移除指定路径git filter-repo --path path/to/big-file.bin --invert-paths执行完之后所有提交的哈希都会变需要强制推送并且通知所有协作者重新克隆。这个操作的破坏性很大团队协作场景下必须提前沟通不是一个人拍脑袋能做的事。还有一个更轻量的选项如果只是想让仓库不再因为这堆历史而变慢可以考虑用 Git LFS 把大文件迁移出去git lfs migrate import --include*.psd这样历史中的大文件会被替换成指针实际内容存在单独的存储里。同样会改写历史同样需要全员配合。6. 高频故障的定位路径几个真实场景复盘6.1 detached HEAD 下提交的代码去哪了有人为了看某个历史版本的文件执行了git checkout 哈希看完顺手改了几行又commit了一下然后git switch main回到主分支回头发现自己刚才的提交不见了。根本原因是那个提交挂在了 detached HEAD 上不属于任何分支。切走之后它就成了不可达对象git log里自然看不到。找回路径git reflog里会有 HEAD{n}: commit: xxx 这样一条记录记下哈希git switch -c 找回的分支名 哈希提交就回来了。整个操作几十秒。预防方法很简单意识到git status里出现 HEAD detached at 的时候如果要在这个基础上做修改先建分支再动手。另外纯查看历史文件其实不需要切换git show 哈希:文件路径直接就能看内容git restore --source哈希 文件能取到工作区都不会让 HEAD 脱离。6.2 提交落到了错误的分支在 main 上直接改代码然后提交了本该在特性分支上做。这时候不要慌也不要reset --hard之后重写一遍。正确做法分两步先git switch -c 正确的分支名在当前提交上建出新分支此时那个提交已经在新分支上了然后git switch maingit reset --hard HEAD~1把 main 退回去。改动完好无损地转移到了正确的分支上。如果落错的是多个提交用git cherry-pick更灵活。先在错误分支上把这些提交用git reset --hard HEAD~n撤掉切到正确分支git cherry-pick 哈希1 哈希2逐个摘过来。cherry-pick 会生成新的提交哈希不同但内容一致注意这个差异在后续排查时可能造成困惑。6.3 合并方向搞反了怎么收拾本该把特性分支合进 main结果在特性分支上执行了git merge main。这时候 main 的内容被合进了特性分支main 本身没变。如果还没推送git reset --hard HEAD~1撤销这次合并即可合并提交只有一个撤销很干净。如果已经有后续提交了用git revert -m 1 合并提交哈希反向抵消。还有一种情况是合并时把冲突解决错了代码逻辑出了问题但已经提交。这时候不需要撤销合并直接在新提交里修正代码更清晰。历史里保留合并 修正这个完整过程比把合并抹掉重来更容易追溯。6.4 本地和远程历史不一致的判断git status提示 Your branch and origin/main have diverged, and have 3 and 5 different commits each说明本地和远程各有对方没有的提交。这时候要看清楚再动手git log --oneline HEAD..origin/main看远程独有的 5 个提交是什么git log --oneline origin/main..HEAD看本地独有的 3 个提交是什么。搞清楚内容之后再决定是 rebase把本地 3 个接到远程 5 个之后历史成直线还是 merge产生一个合并提交双方都保留。如果不加判断直接git pull默认走合并会生成一个合并提交把两条线接起来能用但历史多一个节点。如果配置了pull.rebasetrue走变基本地 3 个提交会被重放哈希全变如果这 3 个提交已经推送到别的分支就麻烦了。所以看到 diverged 提示的时候先看清两边各有什么这个判断动作比选哪个策略更重要。日常排查里git log --oneline --graph --all --decorate这个组合几乎万能。图形化的分支拓扑、所有分支引用、简洁的提交信息一屏展示谁和谁分叉了、哪个标签打在哪个提交上一目了然。我把它配成了别名一天要敲十几次。我在实际使用中最深的一点体会是Git 的命令数量看着吓人但真正需要肌肉记忆的其实只有二十来条剩下的都能从三个区域 快照链 指针这个模型里推出来。每次遇到不认识的命令与其去搜Git 怎么撤销提交这类关键词不如先问自己一句——我要动的是工作区、暂存区还是历史本身这个问题答对了命令基本就锁定在那两三个选项里。另外任何准备执行--hard、--force、rebase这些改历史的操作之前花十秒钟跑一次git status和git rev-parse HEAD把当前哈希记下来这个习惯救过我至少四五次成本极低收益极高。
阅读完成 · 觉得有帮助?