Git 这东西绝大多数人用过一段时间后都会陷入一种状态命令背了一堆问题是真出了事还是慌。分支删错了不知道去哪找提交历史乱成一团不知道怎么收拾线上出了 bug 想定位是哪次提交引入的只能靠肉眼翻日志。我也经历过这个阶段后来花了不少时间把 Git 的对象模型、引用机制这些底层逻辑啃了一遍才发现之前很多所谓“高级命令”其实都是底层原理的自然延伸。不夸张地说理解了那一层东西Git 在你手里才真正从“工具”变成了“武器”。这篇帖子不打算讲基础用法默认你已经能完成日常的 add、commit、push、pull直接聊进阶技巧和藏在它们背后的底层原理每一段我都尽量讲清楚“为什么能这样操作”而不是只丢给你一串命令。1. 先从底层对象模型说起这是所有进阶操作的根1.1 .git 目录里到底藏着什么我们对着某个项目执行git init之后项目目录里会出现一个.git文件夹。很多人从来不看它一眼但几乎所有进阶技巧的答案都在这里面。它的核心成员有几个objects目录存放所有数据对象refs目录存放分支和标签的引用HEAD是一个指向当前引用的文本文件还有index文件记录暂存区状态。通俗点说.git才是一个仓库的“本体”我们平时看到的src、docs这些工作区文件反而是可以被随意替换的“投影”。理解这一点特别重要。因为 Git 在底层几乎不做“差异对比”它做的是“完整快照”。你每次git commit它都会把当下所有被跟踪的文件内容生成一份快照然后存进objects目录。注意是“完整快照”而不是“只存修改的部分”。这和很多人以为的“Git 像网盘同步一样只记录增量”完全不同。当然底层会有压缩和去重内容相同的文件只存一份但逻辑模型就是快照模型。这也是为什么.git目录经常比源码目录还大的原因之一。1.2 三种核心对象blob、tree、commit在objects目录里对象一共分四种类型平时接触最多的是其中三种blob、tree、commit第四种是 tag 标签对象稍后提。先说 blob。你可以把blob理解成“文件内容的一次拍照结果”。它只存内容不存文件名、不存权限、不存路径。随便用一个文件做实验git hash-object 某个文件算出来的哈希就是那个文件内容对应的 blob 的 SHA-1 值。也就是说只有内容相同的文件blob 哈希才相同哪怕两个文件在不同的目录、不同的项目只要内容一模一样blob 就一样。这是 Git 自动去重的基础。tree 对象解决的是“文件名和目录结构放哪里”的问题。一个 tree 对象里记录的信息相当于一个目录清单里面有哪些子目录、哪些文件每个文件对应哪个 blob文件模式是什么。它把 blob 组织成了树形结构。用生活化的方式理解blob 是书的内容tree 是目录页。commit 对象则是最上面的一张“封皮”它记录的信息包括指向某个 tree 对象的哈希即当时整个项目的根目录快照、父提交的哈希通常一个或两个两个就是合并提交、作者信息、提交时间、提交说明。所以整个链式关系是这样的commit 指向 treetree 指向若干 blob 和子 tree子 tree 再指向更下一层的 blob。为什么要讲这个因为一旦你知道了“提交是不可变的”“提交之间通过父指针串联”你就能理解后面所有高级操作的动机。rebase 本质上是“重做一串新的提交”filter-repo 本质上是“把历史重建一遍”reflog 本质上是在“引用变更的流水账里找回曾经存在过的提交”。没有这个认知这些操作都只是死记硬背。1.3 分支和 HEAD 的本质比你想的简单很多人以为分支是一套复杂的代码分支机制其实在 Git 里分支就是一个 41 字节的文本文件放在refs/heads/目录下。这个文件的内容就是一个提交哈希。分支名就是“指向某个提交的指针”。当你新建一个分支实际上就是创建一个新的文件把它的内容指向当前所在的那个提交。当你在这个分支上做一次新提交Git 会更新这个文件的内容让分支指针指向新的提交。就这么简单。HEAD 则是另一个文本文件正常情况下它的内容是ref: refs/heads/main这种格式意思是“我的指针指向 main 分支的指针”。所以我们常说 HEAD 是“指针的指针”。如果你在命令行里执行git log --oneline看到当前分支的历史那就是从 HEAD 指向的分支指针开始沿着每个提交的父指针一条条往前追溯。tag 对象跟分支的区别在于分支会随着新提交而移动而 tag 指向一个固定提交之后就再也不动通常用于发布版本标记。理解了这套引用模型再看名场景“detached HEAD”就会非常通透——无非就是 HEAD 这个文件没有指向某个分支文件而是直接指向了一个提交哈希。2. 进阶技巧一用 reflog 找回“已经丢掉的提交”2.1 为什么 Git 删了还能找回来背后的机制是什么我先说结论在 Git 里除非你主动执行git gc并且对象已经彻底没有被任何引用指向否则几乎所有的“删除”都能找回来。这句话背后的关键机制就是 reflog全称 reference log中文常翻译为“引用日志”。它记录的是“引用变化的历史”。你在.git/logs目录下可以看到HEAD文件里面一行一行记录着什么时间点、HEAD 从哪个哈希移动到了哪个哈希、操作者是谁、操作说明是什么。同样的refs/heads/main也有对应的日志文件记录 main 分支指针的每次移动。举个特别直观的例子。你git branch test创建了分支 test然后在 test 分支上做了两个提交test 指针从 A 移到了 B再移到 C。这时你git branch -D test把分支删掉了在“普通世界里”test 分支连同它的提交 C、B 都像消失了一样git log再也看不到。但在 reflog 里指针从 A 到 B、B 到 C 的每一次移动都被记录着。提交 C 和 B 本身并没有被删除它们依然躺在objects目录里只是现在没有任何分支或标签指向它们。Git 管这种对象叫“悬空提交”dangling commit。只要 reflog 里还留着它们的位置记录你就有办法把它们找回来挂到一个新分支上。这个机制可以类比成一个回收站加记账本——文件没有被立即物理删除而是被标记成“没人要了”同时每一笔指针移动都有审计日志。所以 Git 社区才有一句流传已久的话“只要你 commit 过就没有真正的删除。”2.2 实际操作演示误删分支后的完整恢复流程下面我模拟一下整个场景。假设当前在 main 分支上工作我新建了一个 feat-login 分支切过去做了两次提交然后“手滑”执行了git branch -D feat-login。此刻我的第一反应不是惊慌而是执行git reflog。输出大致长这样a1b2c3d (HEAD - main) HEAD{0}: checkout: moving from feat-login to main f4e5d6c HEAD{1}: commit: 完成登录页的样式调整 b7a8c9e HEAD{2}: commit: 新增登录接口对接 a1b2c3d HEAD{3}: checkout: moving from main to feat-login看到没有虽然分支已经删掉了但是 reflog 里清清楚楚记录了“HEAD{2} 和 HEAD{1} 这两次提交曾经发生过”。那个f4e5d6c就是第二次提交的哈希。想恢复整个 feat-login 分支只需要执行git branch feat-login f4e5d6c如果此时reflog里提交太多找不到对应条目还可以用git reflog | grep 关键词或者直接git reflog --dateiso看精确时间点。如果你干脆忘了提交哈希只记得大概时间也可以用git branch feat-login HEAD{2}这样的相对索引来恢复。加上-f参数可以强制覆盖同名分支。git fsck是另一张底牌。如果 reflog 条目因为过期被清理了悬空提交对象还残留在对象库里可以执行git fsck --lost-found它会把所有没有被引用的对象列出来。输出里会有一组dangling commit的哈希你可以逐个用git show看看内容是不是你要找的确认后同样用git branch挂上就行。我早期靠这招救回过一个删了整整一周的分支当时同事已经准备从备份重来了结果两分钟搞定。2.3 关于 reflog 的几个冷知识以及必须注意的坑reflog 不是无限期保存的。默认配置下普通“已经没有任何引用指向”的对象会在 90 天后被 Git 的自动垃圾回收机制清理gc.reflogExpire控制 unreachable 对象的过期时间默认 90 天还活着有引用的对象是 90 天不清理。所以一旦发现提交丢了越早处理越好。另外要注意reflog 只记录“本地引用”的变化不会因为你执行git fetch把远端分支信息抓下来就把远端的 reflog 抓过来。换句话说reflog 是极其本地化的一份流水账换了电脑、clone 了新仓库老仓库的 reflog 记录都不会跟着走。还有一个操作值得专门提一下git push --force把远端分支从 A 强行改到 B 之后你本地仓库的 reflog 里依然会留下 A 的记录。所以如果你或者团队成员误操作强推只要有人在强推之前 fetch 过、或者强推前本地的 reflog 里还有对应的提交恢复远端分支也不是不可能——但流程相对麻烦。预防的可靠方案是给远端的受保护分支开启“拒绝强推”规则或者使用--force-with-lease这个更温和的参数它会在强推前先检查远端分支是否还是你最初获取的状态如果不是就直接拒绝推送。这个参数我在生产环境是强制自己用的比裸--force安全太多。3. 进阶技巧二交互式 rebase把提交历史整理得明明白白3.1 rebase 的底层逻辑重放而不是合并先说一个常见的认知误区。很多人把 rebase 理解为“把分支基底换一下”概念上没毛病但容易让人误以为它只是改了一下指针位置。真正的底层过程是Git 会找到当前分支和目标任务分支的共同祖先提交然后把当前分支上从共同祖先之后的所有提交一个个“摘下来”再以目标分支的最新提交为新的基底“重新应用”一遍。这个“重新应用”会产生一系列全新的提交对象它们的哈希值会发生变化因为父提交变了提交的时间戳也变了。举个例子。你在 main 分支上切出 feature 分支做了提交 A1、A2。与此同时 main 上又多了提交 M1、M2。此时执行git rebase mainGit 会找到 feature 与 main 的共同祖先假设是 M0然后把 A1、A2 从链上摘下来按顺序重新提交到 M2 的顶端生成 A1、A2。最终 feature 的历史会呈现一条完美的线性M0 - M1 - M2 - A1 - A2。而git merge main的方式则是保留 A1、A2 原来的哈希额外生成一个合并提交把两条历史并行拼在一起。这就是为什么 rebase 会让历史变得干净整洁。它本质上是在“重写历史”所以不是什么时候都可以乱用的。底层的对象模型决定了 rebase 的代价只要你重写了历史上某个提交它以及它之后的所有提交哈希都会变。任何人只要在这条分支上同步过代码重新同步时就会遇到“远端历史和你本地历史分叉”的局面。3.2 实操演示用 fixup 和 squash 把一团乱的历史压成几个有意义的提交进入交互式 rebase 的姿势是git rebase -i HEAD~3它表示要对当前分支最近 3 个提交进行交互式操作。执行之后会弹出一个编辑器里面从上到下依次列出这 3 个提交每一行最前面是命令词默认都是pick。你可以把pick改成别的命令最常用的几个是squash把当前提交合并到上一个提交并合并提交信息、fixup把当前提交合并到上一个提交但直接丢弃当前提交的信息只保留上一个的、reword改写提交信息、drop删除这条提交、edit停下来修改这个提交。我最常用的场景是这样的本地开发功能时习惯频繁提交比如“写了接口一半”“接口逻辑补全”“调整了样式”“补了注释”。这些提交都很小而且很多是半成品状态推到远端让别人看会显得很碎片化。这时我会执行git rebase -i HEAD~4然后把第 2、3、4 行前面的pick改成fixup。保存退出后4 个小提交就被压成了一个提交信息保留第一条“完成登录功能”。整个过程很快Git 重新按顺序重放这些改动最终得到一个干净的提交。还有一种更精细的场景最近 5 个提交里第 1 个和第 3 个是“同一个逻辑改了两遍”我想把第 3 个合并进第 1 个。在交互式界面里把第 3 行挪到第 1 行下面改成fixup即可。注意交互式 rebase 里的行序就是时间顺序最上面是最早的提交所以想“把后面的合并到前面的”就需要手动调整行的位置。这个操作在编辑器里很直观动一动行就行。关于拆分提交也可以把pick改成editGit 会在那个提交停下后让你git reset HEAD~1把提交拆开重新分次提交留到后面专门写一次。3.3 黄金法则这条线绝对不能碰否则就是灾难现场接触 rebase 前请先把这条规则刻进脑子里永远不要 rebase 那些已经推送到共享远端、并且别人可能已经拉取过的提交。原因我刚才说了rebase 会改变提交哈希等于把你自己和别人的历史“同时推翻”。别人下一次git pullGit 会看到本地的提交跟远端的新提交没有共同祖先或者不对应于是一通合并全部乱掉轻则多出很多重复的提交记录重则冲突满天飞。那什么情况下可以大胆用我自己的原则很简单只对自己还没推送出去的本地提交做 rebase。比如我正在做一个功能已经提交了 5 个小提交但一个都没 push这 5 个就是我的“私有财产”随便 squash、reword、drop 都行。等整理完成再一次性 push 到远端。另外在准备把 feature 合并回 main 之前我通常也会git rebase main把 main 上的新提交先接过来这样能提前解决冲突避免最终 merge 时把人堆到一起。但前提是 feature 分支没有别人在协同开发。如果多人在同一个 feature 分支上干活那就放弃改造历史老老实实用 merge保留真实的分叉记录反而更容易协作。4. 进阶技巧三bisect 二分定位再也不怕“最近哪个提交搞坏了东西”4.1 二分查找原理在 Git 里的应用为什么又快又准排查 bug 最怕的就是项目历史长、提交多而且你完全不知道问题从哪个版本开始出现。比如线上突然发现文件上传功能挂了你只知道上个月还好好的。这种情况下人肉翻提交日志是很绝望的但 Git 提供了一个几乎完美的自动化工具git bisect。它的底层原理特别朴素就是二分查找法。给你 N 个提交串成一条链你在某个点标记“这是好的”在另一个点标记“这是坏的”Git 就会自动在这两个点中间选一个中点的提交切过去让你验证当前是好是坏。根据你的回答它再把范围缩小一半继续二分。时间复杂度是 O(log₂N)12 个提交只需要 4 次判断1000 个提交只需要 10 次左右。这就是为什么 bisect 在处理超长历史时效率极高。你想一下如果是 3 个月的提交历史几百次提交人肉排查可能要一下午bisect 只需要按几次回车就锁定了目标。它利用的正是提交链天然就是一条有序列表这个特性二分查找的前提是“有序”。从好到坏的发展是单调的——一旦某个提交引入问题那么它之后的所有提交理论上都包含这个问题除非后续又修了。如果中间有提交反复修改过同一个文件导致问题时好时坏bisect 的判定可能混乱就需要适当的 skip 操作来跳过那些无法验证的提交。4.2 实操演示手动二分和自动化 bisect run 两种玩法手动二分的过程是这样的。先告诉 Git 当前的提交是坏的再告诉它某个历史版本是好的git bisect start git bisect bad # 当前处于坏的状态 git bisect good v1.2.0 # 假设 v1.2.0 是好的Git 会立刻检出中间位置的提交并输出类似“大概还剩 3 步”的信息。你就在这个代码状态下验证问题是否存在。如果能复现 bug执行git bisect bad如果没问题执行git bisect good。它会继续二分直到最后给出结论第一个出问题的提交是哪一个。结束后执行git bisect reset回到原来的分支。如果验证过程可以写成脚本那就直接自动化。比如问题可以靠一条命令验证——有一个脚本test_upload.sh退出码 0 表示正常非 0 表示不正常。那么整个 bisect 可以一路撒手git bisect start HEAD v1.2.0 git bisect run bash test_upload.shGit 会自动跑脚本根据退出码自动标记 good/bad继续二分直到找到引入问题的提交。我实测过很有感觉的场景旧项目从远古版本到现在的提交有一千多个bisect run 一条命令跑下来几分钟就锁定了是某一次升级依赖时把上传接口的地址写错了。这要是人工翻真的会翻到怀疑人生。还可以加--first-parent之类的参数来限制只沿主干查找git bisect start --first-parent适合主干很好但合并进来的分支历史非常庞杂的仓库。它会忽略合并提交的所有分叉只看每一个 merge commit 本身速度会更快定位也更符合“哪个合入点引入了问题”这个视角。4.3 自动化 bisect 时容易踩的坑和提速技巧先说脚本判定的标准。bisect run里脚本的退出码是有讲究的0 表示当前提交是“好”的125 表示当前提交无法测试比如编译不过1 到 127 之间其他非 0 值表示“坏”。注意 125 这个特殊值如果某个提交因为环境原因编译失败不标记它好坏而是 mark 成 skip让 bisect 跳过它去找别的提交来缩小范围。如果你脚本里根本没有处理编译失败的情况很可能一编译不过就被判成“坏”导致定位结果不准确。第二个常见的坑是自动化构建过程耗时太长。每次 bisect 切换提交后都要重新编译如果项目很大编译十几分钟十几步下来也得几个小时。提速思路有几个优先保证构建脚本增量编译生效减少无用的全量构建先把范围压缩到可疑区间再开始 bisect比如先用 git log 看看大致的改动时间线对某些依赖外部服务的测试可以写成 mock 模式避免每次验证都被网络延迟拖死。还有个很有用的技巧如果已经知道出问题的提交肯定在某几个目录里改过可以先git bisect start HEAD v1.2.0 -- src/services让 bisect 只管那些影响过src/services目录的提交其他提交直接跳过能大幅减少中间步骤。5. 进阶技巧四用 filter-repo 重写历史清理敏感信息和巨型文件5.1 为什么历史清理不是“删掉重提”那么简单有这样一个常见事故某天发现某个配置文件带着数据库密码被提交进了仓库并且已经推送到了远端。很多人第一反应是“把文件删掉再提交一次”就行了。大错特错。因为 Git 的对象是不可变的那个包含密码的提交对象仍然躺在历史里任何人只要git clone整个仓库再翻翻历史密码照样重现。同样的问题也适用于误提交的大文件比如不小心提交了一个 500MB 的压缩包。就算你在最新提交里删掉了.git目录里那个 blob 对象依然还在克隆仓库时还是会把整段历史拉下来体积爆炸。要“从根上治愈”必须重写历史把所有提到这个文件的提交全部替换掉。这个过程早期靠git filter-branch来做但那玩意儿又慢又难用官方文档自己都写着“请小心使用”因为它在重写历史时很容易遇到各种远古提交的坑。现在的首选工具是 git-filter-repo一个开源 Python 工具。它的设计思路更现代功能也更全支持按路径删除、按内容替换、移除特定作者、重命名路径、压缩历史里的大文件等等。Git 官方文档在推荐替代方案时挂的就是它。这套底层逻辑依然是对象模型你不用“修改”某个提交而是从仓库里的所有提交出发重新构建一连串新的提交对象替换掉那些要删除的引用最后删掉旧的对象。所以重写历史之后所有的提交哈希都会改变所有 clone 过这个仓库的人都会和远端历史脱节。5.2 安装与基础用法三步完成一次干净的历史清创git-filter-repo 的安装需要 Python 环境Windows、macOS、Linux 都能装。简单的方式是用 pippip install git-filter-repo装完确认一下git filter-repo --version能出输出版本号就行。注意它和原生命令git filter-branch不是一个东西调用时是单独的git filter-repo命令。最常见的需求是删除某个文件的所有历史痕迹。假设误提交了一个config/secrets.yml想把它从整个历史中连根拔掉最基础的命令长这样git filter-repo --path config/secrets.yml --invert-paths--path指定要处理的路径--invert-paths表示“反过来排除掉这个路径”合起来意思就是“保留除了这个路径以外的一切”。执行完毕后Git 会重写所有历史提交secrets.yml在历史里彻底消失。如果想清理多个文件也可以写多组--path。如果还要替换历史中的密码字符串用--replace-text它接受一个文件文件里每行是一对替换规则例如# replacements.txt pssw0rd123REDACTED然后执行git filter-repo --replace-text replacements.txt。Git 会在所有历史 blob 里做文本替换把旧密码换成占位符。这个操作用于应对那种“不止一个文件很多文件里都出现过同一个密码”的情况。重写完之后记得强制推送并且让所有协作者重新克隆或者做一次彻底的同步。这个过程要明确告诉团队——旧历史已作废不要试图基于旧历史继续开发。5.3 重写历史后的团队协作流程这一步没做好会出大乱子技术操作本身不难难的是后续协调。我总结出来一套相对稳妥的流程分享给大家参考。第一步重写前先冻结提交和团队约定一个时间窗口所有人都不要往远端推新提交。第二步在本地重写历史完成后用git push --force --all强制推送所有分支再把受影响的 tag 也强推一下git push --force --tags。第三步让所有协作者执行如下操作的组合先备份自己的本地分支方便万一还要找回然后执行git fetch origin、git reset --hard origin/main之类的方式把本地分支硬重置到远端新状态。如果有些本地分支是基于旧历史开发的最干净的方法是删掉本地分支重新git checkout -b从远端创建。要反复提醒团队“先备份再重置”否则本地那些还没推送的提交也会跟着没了。另外提一个容易被忽略的问题filter-repo 默认会把远端信息抹掉这是官方出于隐私考虑的设计。重写后你会惊讶地发现git remote -v什么都没有了。需要手动重新加一下远端git remote add origin 仓库地址然后再推。第一次用这个工具的人十有八九会在这里卡一下提前知道能少踩一个坑。6. 常见问题与排查技巧实录6.1 detached HEAD 是什么到底该怎么理解怎么处理不少人在网上看到了“在某个 tag 上改代码”之类的操作执行完git checkout v1.0之后系统提示HEAD detached at v1.0顿时慌得不行。其实这个词翻译成“游离的 HEAD”确实吓人但本质非常简单正常情况下 HEAD 文件内容是一行“指向某分支”的文本而此时它的内容是“直接指向某个提交哈希”。换句话说你不再位于任何一个分支上。你在这种情况下做的新提交不会更新任何分支指针只会在 reflog 里留下记录。如果你只是看看代码检出一个历史版本随便看看看完切回来就行git switch -一秒回到上一个分支。如果你真的在这个状态下做了修改并且想保留务必立刻创建一个分支把当前提交挂住git switch -c temp-branch。这样新提交就落到了一个真正有名字的分支上不会被 garbage collector 当垃圾清掉。这条经验我见过不下十个人踩进去最常见的教训是在 detached 状态改了一堆东西切走之后再切回来发现改动全“没”了然后四处求救。其实改动都还在 reflog 里但普通人根本不知道去哪翻。所以我的建议是怀疑自己处于 detached HEAD 时第一时间跑git status看有没有 “HEAD detached” 字样有的话先想清楚这段修改还要不要再动分支。6.2 merge 和 rebase 到底选哪个别再纠结了这是社区里万年吵不完的话题。我给出的选择逻辑非常简单直接。如果这是一个“长期存在的协同开发分支”比如多人并行在 feature 分支上开发那就坚决用 merge。保留真实的合并提交和分叉历史有利于团队看清楚每个人做了什么、合入的时间点是什么。如果你想保持历史线性清晰、看起来优雅那就只处理自己的私有分支——在推送之前把它们整理成一个或多个语义清晰的提交。一句话没推出去的随便重塑推出去被别人拉过就不要碰公共分支永远用 merge。还有一个补充场景当你准备把自己负责的 feature 分支跟最新的 main 同步时与其git merge main让 feature 里出现一堆合并点不如git rebase main把 feature 上的提交整齐地移动到 main 顶端。这样最后git merge feature时 Git 能直接快进合并历史是一条直线。这操作的前提就是确认 feature 分支上没有别的协作者。如果四个人在同一分支上各推各的那就老老实实 merge别折腾了。6.3 误操作后悔药速查表我把自己这几年真正用过的“后悔药”场景整理成了一张速查表放在这当结尾吧。这张表里的每一项都对应前面讲过的底层机制实战时直接照着抄就行。场景推荐命令原理说明误删本地分支git reflog找到哈希后git branch 分支名 哈希reflog 记录了引用指针的移动历史误提交敏感信息已推送git filter-repo --path 路径 --invert-paths重写历史让旧 blob 不再被任何提交引用误提交大文件已推送git filter-repo --strip-blobs-bigger-than 5M移除超限 blob直接瘦身仓库改错了最新提交信息git commit --amend用新提交替换旧提交旧提交进 reflog想把最近 N 个小提交合并成 1 个git rebase -i HEAD~N利用交互式 rebase 重放并合并提交想在某个历史提交上加修改git rebase -i 起点后把 pick 改成 edit停在指定提交修改后 ammend 并 continue误执行了硬重置git reflog找回重置前的提交哈希reflog 会记录 reset 前后的指针位置想找回“所有悬空提交”git fsck --lost-found扫描对象库里所有未被引用的对象最后多提一句工具永远在升级命令也会变但这些操作背后那套“不可变对象指针移动引用日志”的模型到目前为止没有任何变化。我个人的建议是把本文涉及的底层概念自己动手跑一遍建一个测试仓库故意制造误删和悬空提交然后一步步找回来。实践过一遍之后你对 Git 的理解就不会停留在“背命令”的层面了遇到任何奇异情形你都能通过观察.git的真实状态找到出路。希望这篇东西对你有用。