ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Git Merge与Rebase:彻底搞懂合并与变基

Git Merge与Rebase:彻底搞懂合并与变基 平时带项目我最担心听到的往往不是“代码有 bug”而是“我合并完代码之后历史怎么变成这样了”。Git 本身是个极好用的工具但两个核心合并命令——Merge 和 Rebase——几乎每个团队都会用真正能把两者讲清楚的人却不多。我见过很多开发者把 merge 当万能药把 rebase 当洪水猛兽也见过反过来的rebase 用得飞起结果某天把同事已经推送的提交弄丢群里吵到凌晨。这篇文章的目标是帮你彻底建立对 Merge 和 Rebase 的判断力。读完你会清楚底层逻辑上它们分别怎么工作什么场景选 merge、什么场景选 rebase以及操作失误之后怎样快速救回来。不管你是刚装好 Git 还在折腾配置的新手还是已经在 IDE 里点过无数次 Merge 按钮的“熟练工”这篇文章都值得花二十分钟认真过一遍。1. 为什么合并这么难先理解 Git 提交记录的底层模型1.1 提交到底存的是什么快照而非文件差异列表很多人在 SVN 时代养成了根深蒂固的习惯以为每次提交记录里存的是“这次改了什么”。但 Git 的commit对象里存的是完整快照不是差异补丁。每个提交包含一棵目录树tree、作者信息、提交者信息、提交说明以及指向父提交的指针。你平时在 GitHub 上看到的每次提交改动是 Git 拿当前快照与父提交快照动态对比后算出来的结果并不是提交本身自带一份 diff。这个认知直接影响你对合并的理解既然每个提交都是那一刻的完整项目状态那么合并两个分支本质上不是“把两个 diff 叠在一起”而是“把两条分叉的历史整合成一条继续前进的历史”。你可以把提交想象成每个学期期末拍的集体照SVN 式记录则像是作业本上的“某页第几行改了”。集体照保留了那一瞬间的完整状态事后想知道变化把两张照片摆在一起对比就行。这也是 Git 分支切换和合并能做得这么快的原因之一——多数操作只是移动指针而不是搬运文件。1.2 分支不是文件夹而是一个会移动的指针搞清楚分支的本质很多困惑会瞬间消失。创建分支时Git 并不会复制你的代码文件它只是在.git/refs/heads/下新建一个文件里面写入当前提交的哈希值。所以分支本质上是“贴在某个提交上的标签”而 HEAD 则是指向当前分支的指针。切换分支就是移动 HEAD 指针并更新工作区文件。用读书来类比一本书从同一个起点开始你翻到第 100 页同事翻到第 200 页你们各拿一个书签。合并就是把两个书签位置之后的内容重新整合Rebase则是把一个书签后面的所有页“抄录”到另一个书签后面最终看起来你从一开始就一直读到了第 200 页。没有这个“指针标签”的心智模型后面讲的三方合并、快进合并、变基重放全都容易绕晕。1.3 Merge 和 Rebase 的本质区别记录分叉 vs 抹平分叉用一句话概括两者差异Merge 说“这两条线在这里汇合了”Rebase 说“从头到尾只有一条线我本来就该长在这里”。Merge 在整合两个分支的同时保留了“它们曾经分叉过”的事实会生成一个带有两个父提交的合并提交Rebase 则是把当前分支上的每个提交逐个“重放”到目标分支顶端让最终历史的形状变成一条直线看不出中途曾经分叉过。所以选 merge 还是 rebase本质上是在回答一个问题你希不希望提交历史里保留这条分叉的痕迹1.4 四个必须懂的关键词后续文章会反复用到这几个术语先统一认识merge-base两个分支的最近共同祖先提交也是三方合并计算修改的基准线。ours / theirs合并时ours 指当前所在分支theirs 指被合入的分支。注意在 rebase 过程中这两个称呼会交换容易踩坑。fast-forward快进当一条分支是另一条分支的直接延伸时Git 可以不生成合并提交直接把指针往前移动。ORIG_HEADGit 在执行 merge、rebase、reset 前会记录原来的 HEAD 位置用于操作后的回退。2. Merge 到底做了什么一次合并背后的三方运算2.1 为什么需要三方合并假设两个分支从同一个提交分叉各自修改了不同文件。如果你只对比当前分支和目标分支这两份文件完全无法判断哪些内容是“另一边刚改的”哪些是“本来就有但恰好不同”的。Git 必须找到共同祖先作为参照系这就是merge-base。三方合并的逻辑是以共同祖先为基准分别看两边相对于它做了哪些修改。两边改不同位置自动合并两边改了同一处就产生冲突。这个场景你每天都会遇到——你改了文档第一段同事改了第五段拼起来没问题但两个人同时改第一句话就必须坐下来商量。Git 的默认合并策略现在叫 ort它比早期的 recursive 更聪明能处理文件重命名、模式冲突等情况但理解三方模型已经足够解释 90% 的冲突产生原因。对比对象含义在合并中的作用merge-base两个分支的共同祖先提交计算两边修改的基准ours当前所在分支的版本合并后保留的一侧theirs被合入分支的版本合入带来的一侧2.2 一次 merge 产生的提交记录长什么样在 develop 分支上执行git merge feature/login通常会有两种输出。第一种如果 feature/login 恰好是从 develop 最新提交延伸出去的Git 会走 fast-forward 路径直接把 develop 指针移动到 feature/login 的顶端不生成额外提交。第二种如果历史已经分叉Git 会做一次真实的三方合并生成一个合并提交。输出一般长这样Merge made by the ort strategy.用git log --graph --oneline看提交图类似* 6a5f3c2 (HEAD - develop) Merge branch feature/login into develop |\ | * 3f7d9a1 (feature/login) 完成登录接口测试 | * cfbd820 实现第三方登录回调 * | 4e2b8c7 修复首页轮播图缓存 * | 8a30f11 调整服务端接口鉴权 |/ * 7d9012a 初始化项目注意看顶部这个合并提交6a5f3c2它有两条父链一条指向 develop 自己的历史一条指向 feature/login。这就是 merge 的核心特征——它不修改任何已有提交只是新增一个节点把两条线接起来。2.3 Merge 擅长的地方公共分支与可回滚性正因为 merge 不会改写历史它天然适合公共分支。main、develop、release 这类分支上所有提交都已经被团队成员拉取过任何“重写”都会造成大面积混乱。用 merge 整合功能分支保留了完整的时间线一次功能合入发生在什么时候、包含了哪些提交回溯时一目了然。还有一个非常实用的优势可回滚性。假设你通过 merge 把一个 feature 合入了 main上线后发现问题需要临时回滚一条命令就能解决git revert -m 1 merge-commit的哈希-m 1表示保留第一个父提交即 main 当前版本把第二个父提交feature 分支带来的改动整体撤销。如果用 rebase 把 20 个提交整整齐齐地重放进 main想回滚这一批功能就得逐个 revert 或费劲地确定范围复杂度完全不在一个量级。2.4 Merge 的混乱面为什么提交图会越看越乱merge 最大的代价是历史噪声。如果团队里每个人都频繁从 main 合并最新代码提交图会迅速变成一张“毛线球”到处是Merge branch main into feature-xxx这样的节点。这类合并提交往往没有任何信息量纯粹是因为分支不同步而“不得不合并”的产物。更麻烦的是普通git log里看合并提交无法直接看到它到底改了哪些文件要跟两个父提交分别对比。对于追求“历史即文档”的团队来说这种杂乱是不能接受的。这也是很多人转向 rebase 的根本原因——不是因为 merge 做错了什么而是他们想要一条更干净的历史线。3. Rebase 到底做了什么一次重放背后的历史重写3.1 变基的内部流程把提交拆开再铺回去Rebase 这个词可以拆成 re base意思是“重新设置基底”。git rebase 目标分支做的事情是把当前分支的分叉点从原来的共同祖先挪到目标分支的最新提交上然后把当前分支自共同祖先以来的所有提交一个个按顺序重新应用一遍。内部流程拆开看是四步找到当前分支与目标分支的共同祖先把当前分支从共同祖先以来的提交保存成临时补丁序列把当前分支指针硬重置到目标分支最新提交逐个重新应用补丁。如果某一步发生冲突重放会暂停等你解决后继续。所以 rebase 相当于把“一次大型合并”拆成了“多次小型合并”。生活化理解你本来从老家出发走老路进城途中绕了不少弯。Rebase 相当于把这条路一段一段拆下来整体铺到一条新建的高速公路上最终看起来你一开始就在走那条新路。3.2 重放之后的提交已经不是原来的提交这是 rebase 最容易让人忽略的底层事实提交的父提交变了Git 算出来的 commit hash 就必然变了。哪怕文件内容完全一样重放后的提交和原来也不是同一个对象。同时committer date 会更新为重新生成的时间author date 通常保留。属性Rebase 前Rebase 后commit hash987abc3f7d9a1父提交旧基础新基础author date保留保留committer date旧时间重放时的新时间理解了这一点“不要对已经推送到共享远端的分支 rebase”就不再是纪律要求而是逻辑必然——你一旦重写了别人已经拉取过的提交同事本地的那份历史就和远端对不上了下次 pull 会看到大量“凭空消失又出现”的提交严重时直接导致对方的工作丢失。3.3 交互式 Rebase整理提交的杀手锏单纯git rebase只是线性化真正发挥威力的是交互模式git rebase -i HEAD~3这条命令会在编辑器里列出最近三个提交并提示你每个提交前可以放什么指令。最常用的是这几个pick保留reword保留但改提交信息squash合并进上一个提交fixup合并进上一个提交且丢弃上一个提交的消息drop删除。举个例子。功能分支上有三个提交实现登录、修复登录 bug、补一个测试。从代码评审的角度这三个提交其实是一个完整功能最好在合入主干前合并成一个清晰提交。交互式 rebase 中这样写pick c1e3a2 实现登录 squash b7f8cd 修复登录 bug fixup d014aa 补一个测试保存退出后三个杂乱提交会变成一个干净的提交。这个能力让 rebase 成为个人分支整理历史的利器也是代码评审体验的核心保障之一。3.4 Rebase 的适用边界判断标准只有一条面对任何“该不该 rebase”的疑问判断标准只有一个这个分支是否已经被别人共享使用了如果分支只存在于你的本地仓库那么随便 rebase随便用交互模式整理都不会影响他人。常见的git pull --rebase也属于安全场景本地有未推送提交远端有新提交用 rebase 把本地提交叠加到远端最新提交后面能避免产生毫无意义的 merge 提交历史保持线性。但如果分支已经推送到远端且同事可能已经基于它开发就不要通过 rebase 来重写它。4. Merge vs Rebase提交图、冲突与远端影响的硬核对比4.1 提交图对照分叉保留 vs 历史线性假设 feature 分支有两个提交main 分支也有两个新提交。merge 之后的历史* 合并提交 M |\ | * C2feature 新提交 | * C1 * | B2main 新提交 * | B1 |/ * B0rebase 之后的历史* C2feature 之前的提交重放后新 hash * C1 * B2 * B1 * B0两者都是“把 feature 的内容合进 main”但呈现出来的故事完全不同。表格总结更直观维度MergeRebase历史形状保留分叉新增合并节点抹平分叉呈现线性是否重写提交不修改任何已有提交重写当前分支的提交冲突处理体验一次性集中处理逐提交处理可能反复冲突推送方式普通 push 即可通常需要 force push回滚功能复杂度低revert 合并提交即可高需要反向操作多个提交历史可读性完整还原开发轨迹干净聚焦适合读代码的人4.2 冲突处理体验一次硬仗 vs 连续小仗Merge 的冲突处理是“一次性通知”它会在最终合并时把所有冲突文件列出来你集中解决完提交一次就结束。适合两个分支整体差异不大、冲突面有限的情况。Rebase 的冲突处理则是一层一层剥洋葱重放第一个提交时冲突解决重放第二个提交时可能同一个文件又冲突再解决。如果你 rebase 一个包含 10 个提交的长分支而目标分支改动又很大同一处冲突重复出现的概率很高非常消磨耐心。我遇到过一种实用的“先 merge 再 rebase”组合打法先git merge 目标分支把主要冲突一次性解决掉再git rebase 目标分支这样后续重放时冲突会大幅减少。另外一个容易被忽略的宝藏功能叫 rererereuse recorded resolution开启后 Git 会记住你解决过的冲突方案之后遇到相同冲突自动应用。配置只有一行git config --global rerere.enabled true靠这一个配置能把 rebase 多提交反复冲突的痛苦降低大半。4.3 对远端的影响普通推送与强制推送Merge 之后推送远端属于“只新增提交”的操作用普通git push就能完成远端历史只会向前走不会破坏任何人的工作。Rebase 之后推送则不同因为当前分支的提交 hash 被重写远端会拒绝普通推送你必须使用强制推送git push --force-with-lease注意这里强烈不建议用裸的git push -f。--force-with-lease会在推送前检查远端是否在你上次拉取之后发生了变化如果有人已经推进了新的提交它会拒绝覆盖相当于给强制推送加了最后一道保险。裸git push -f没有这道检查是众多“同事提交被覆盖”事故的根源。很多代码托管平台会在远端挂 pre-receive hook 拦截这类非快进推送你看到的那些 “pre-receive hook declined”、“another open merge request already exists” 提示本质都是在用技术手段保护共享历史不被重写。4.4 从“历史可变性”看安全本质Merge 的安全感来自“历史不可变”你无法通过 merge 意外销毁任何人的提交。Rebase 的收益来自“历史可整理”你能够把一堆杂乱提交变成逻辑清晰的序列。于是核心策略得到一条非常清晰的主线属于个人的、未共享的历史用 rebase 整理得更精彩属于团队的、已共享的历史用 merge 保持得更稳定。把这条主线想明白80% 的 Git 合并事故都可以避免。5. 到底该选谁场景化决策与团队规范落地5.1 个人开发先 Rebase 整理再 Merge 集成我在本地开发时习惯很明确功能分支上先随便提交中间可能会有wip、fix typo、临时调试这类乱七八糟的提交。准备推送或开 Pull Request 之前我会用git rebase -i把这些提交整理成三到五个语义清晰、符合开发顺序的提交然后用git push -u origin 分支名推送。如果开发过程中 main 分支有新提交我会用git rebase origin/main把本地功能重新铺到最新主干上而不是用 merge因为我不希望在个人功能链上留下多余的分叉节点。这里有个重要提醒一旦分支推送过、你又基于这个分支创建了子分支就不要再随意 rebase 了除非你清楚所有关联分支的指向。5.2 团队协作共享分支禁 Rebase公共分支用 Merge团队协作的规则应该比个人开发严格得多。共享主线main、develop、release上禁止 rebase 和 force push这不是技术问题而是团队信用问题。别人已经基于你的提交开始工作后你重写历史相当于在别人的地基下抽掉承重墙。功能分支合并回共享主干时推荐使用git merge --no-ff feature/xxx。--no-ff的作用是强制生成一个合并提交即使技术上空可以 fast-forward。为什么非要多出一个提交因为合并提交是一个明确的“功能边界”以后回溯“这个功能是什么时候进来的、包含哪些改动”会容易得多。我见过太多团队用默认 merge碰上刚好能快进的情况功能提交就悄悄嵌进了主干历史事后要找出该功能的所有相关提交很费劲。5.3 开源平台Pull Request 的三种合入方式GitHub、GitLab 这类平台在合入 PR 时通常提供三种方式它们本质上是 merge 与 rebase 思想的产品化合入方式原理适用场景Create a merge commit保留 PR 所有提交新增一个合并提交希望完整保留开发脉络Squash and merge把 PR 所有提交压缩成一个提交面向“主干只求简洁”的历史Rebase and merge把 PR 提交在目标分支顶端重放想让主干保持线性且保留每个提交开源项目里 squash and merge 很流行因为能避免 main 分支被大量wip提交污染。但要注意squash 之后无法在分支粒度上追踪原始提交这需要团队根据自身取舍。公司内部我通常用 merge commit因为它和内部审计、发布回溯的需求更匹配。5.4 一张表帮你做决策场景推荐操作理由本地未推送的功能分支整理git rebase -igit rebase历史可读风险可控拉取远端最新代码git pull --rebase避免产生无意义 merge 节点功能分支合入公共 develop/maingit merge --no-ff保留功能边界不重写历史上线后需要临时回滚一个功能git revert -m 1 merge-commit一次性安全回滚开源项目提交贡献遵循仓库规定的 PR 合入方式与社区约定保持一致5.5 一套可以直接抄的团队 Git 规范结合上面所有判断我沉淀了一套适合大多数中小团队的规范可以直接复制使用main 分支为受保护分支禁止直接 push禁止强制推送。所有功能开发从 main 创建独立分支提交信息遵循统一约定。开发期间定期git fetch后用git rebase origin/main更新基础减少最后合入的冲突。功能分支评审通过后合入 main 统一使用git merge --no-ff生成合并提交。紧急修复分支同样遵循该流程不允许绕道。任何情况下都不允许对已经被他人使用的分支历史执行 rebase 或git push -f。这套规范的核心不是限制自由而是把“个人分支自由整理、共享分支绝对稳定”这个原则固化下来。6. 零坑操作常用命令、撤销救援与事故预防6.1 高频命令参考表先给一张速查表都是平时最高频的命令参数含义我会在后续小节展开命令作用git merge --no-ff branch强制生成合并提交保留功能边界git rebase branch将当前分支提交重放到目标分支顶端git rebase -i HEAD~n交互式整理最近 n 个提交git pull --rebase拉取远端时用 rebase 避免 merge 节点git revert -m 1 merge-commit安全撤销一个已推送的合并提交git reset --hard ORIG_HEAD回退到 merge/rebase 之前的 HEADgit reflog查看 HEAD 的所有历史移动记录git push --force-with-lease带检查的强制推送避免覆盖他人工作6.2 撤销 Merge 的两种救援路径根据 merge 是否已经推送处理方式完全不同。如果刚在本地执行完 merge 还没推送最方便的回退是git reset --hard ORIG_HEADORIG_HEAD 保存了 merge 执行前 HEAD 的位置一条命令回到合并前干净利落。如果 merge 已经推送到公共远端绝对不能 reset 后强制推送因为那会重写公共历史。正确做法是git revert -m 1 merge-commit它会生成一个反向提交把合并引入的改动撤销但保留 merge 提交本身。这样历史是“前进”的其他同事 pull 时不会产生任何难以同步的问题。我个人建议团队统一用 revert 处理上线事故因为 revert 可追溯、可再次 revert 恢复风险完全可控。6.3 撤销 Rebase 与 Reflog 救援Rebase 出错或reb到一半心态崩了第一反应是看git reflog。这个命令会记录 HEAD 的每一次移动包括 rebase、reset、checkout 前的状态。假设 rebase 前的位置在列表里显示为HEAD{5}git reflog git reset --hard HEAD{5}就能回到 rebase 之前的状态。reflog 默认保存 90 天只要不手动执行git gc一般都能找回来。但注意远端已推送并被覆盖的提交其他人依赖的找回会非常困难这就是为什么每次 rebase 前都要慎重确认分支是否共享。我的压箱底习惯是不管操作前多么自信rebase 整理前一定先建一个备份分支git branch backup/feature-xxx一条命令买一份保险。这个动作成本极低却能让所有实验性操作变得可逆强烈建议每个人养成这个习惯。6.4 五个我见过的 Git 事故这些年围观和亲历过不少合并事故挑五个最有代表性的分享出来。事故一IDE 里一键 rebase看到冲突后随手把所有文件 add 并继续结果冲突标记 HEAD残留在代码里编译直接失败还找不到原因。排查这类问题很简单先全项目搜索或确认无遗留再继续。事故二有人对公共 main 分支执行 rebase 后强推同事基于旧历史提交的代码从远端消失最后靠 reflog 加手工 cherry-pick 才救回来浪费了整整一天。事故三rebase 中途心态炸裂直接用了git checkout .或git clean -fd清掉冲突文件结果把未完成的冲突修改也一起清掉了。正确的放弃姿势是git rebase --abort任何情况下都不要手动清文件。事故四一直用默认git pull提交历史里攒了几十个Merge branch main into 功能分支的空转合并。这类提交毫无信息量是历史噪声的主要来源。事故五依赖 IDE 一键操作出了问题完全不知道执行了什么命令无从排查。我一般建议关键合并操作至少在命令行跑一遍看到完整输出出错时才知道往哪里追。6.5 值得设置的三个 Git 配置项git config --global pull.rebase true git config --global rerere.enabled true git config --global alias.lg log --graph --prettyformat:%h %s --all第一个配置让git pull默认变成git pull --rebase避免日常拉取产生多余 merge 节点。第二个配置是前面说的冲突解决记忆功能。第三个配置提供一个美观的提交图git lg一下就能看清分支拓扑。注意pull.rebase true对不熟悉 rebase 的同事会有一定理解门槛建议先在自己的开发机上用熟再考虑推广到团队避免一次性引入大量认知负担。7. 一次真实冲突的处理全流程Merge 与 Rebase 实战记录7.1 一次合并任务的完整开场用我最近一个真实的项目场景做演练。项目里 develop 分支已经领先 main我在本地还有一个 feature/payment 分支包含第三方支付对接的若干提交。现在要把 feature/payment 合入 develop完整操作如下git checkout develop git pull --rebase origin develop git merge --no-ff feature/payment第一条切分支第二条用 rebase 方式更新本地 develop第三条合入功能并强制生成合并提交。正常情况下到达第三条时输出会提示合并成功但开发中哪有一次这么顺的很快冲突就来了。7.2 冲突提示文本该怎么读合并失败时Git 的输出大概长这样Auto-merging src/checkout.js CONFLICT (content): Merge conflict in src/checkout.js Automatic merge failed; fix conflicts and then commit the result.这里的每一行信息都有用第一行告诉你哪些文件被自动合并了第二行是冲突点第三行是当前状态。随后敲git status可以看到冲突文件列在 “Unmerged paths” 区域。很多新手看到这个界面就慌其实只需要做三件事找到冲突文件、解决冲突、继续操作。7.3 Merge 冲突解决全程演示打开src/checkout.js你会看到类似下面的冲突标记 HEAD const gateway stripe; const gateway alipay; feature/payment HEAD到之间是当前 develop 分支的版本到 feature/payment之间是被合入分支的版本。这时需要判断保留哪个两个都要还是要写一段新的逻辑确认后删除整个冲突标记保存文件。接着执行git add src/checkout.js git merge --continueGit 会打开编辑器让你填合并信息通常默认生成 “Merge branch feature/payment into develop”保存退出即可。完成后用git log --graph --oneline验证确认合并提交和两条父链都在。这里有个细节值得强调删冲突标记的时候不少新手会把有效代码一起删掉尤其是两段代码又有相似部分时肉眼极难发现。稳妥的做法是保存后用编辑器高亮标记再检查一遍或者直接搜索。7.4 Rebase 冲突解决的差异点现在换一个场景假设我在本地功能分支上想用 rebase 把 develop 的最新提交铺到自己的分支下git checkout feature/payment git rebase develop如果冲突发生流程和 merge 解决冲突后的第一步相同打开文件、处理标记、git add。但下一步不同不是git merge --continue而是git rebase --continue如果这个分支上有多个提交而冲突发生在第一个提交解决完成后 rebase 会继续重放下一个提交如果那个提交也触及同一处逻辑就会再次停下、再次要求解决。这是前面说过的“连续小仗”体验。中途如果想彻底放弃执行git rebase --abort就能回到 rebase 之前的状态前提是不要手动清过文件。7.5 三个容易忽略的冲突细节二进制文件冲突是最让人头疼的。图片、Excel、PDF 这类文件没有文本内容可以手动编辑Git 无法帮我们“合并”只能二选一。处理命令是git checkout --ours 文件 # 保留当前分支版本 git checkout --theirs 文件 # 保留被合入版本注意还是那句老话在 rebase 过程中ours 和 theirs 的指向会交换选之前一定要看清楚当前操作是 merge 还是 rebase。第二个细节是“重现的冲突”。你明明解决过某个冲突rebase 到下一个提交又遇到了这时候 rerere 就是救星。如果没提前开启也只能耐着性子继续解决。第三个细节是外部比对工具。很多人喜欢用 Beyond Compare、VS Code 的可视化合并界面确实比纯文本编辑直观。但可视化工具保存后一定要回编辑器确认冲突标记是否全部清除否则最后提交里可能残留一行这类低级错误我见过不少回。文章写到这正好用自己的实际体会做个收尾。我现在的个人习惯非常明确还没推送过的个人分支随便 rebase当作草稿随意整理一旦分支推送过、或者被团队共享就只用 merge绝不重写历史。这个原则帮我躲过了绝大多数 Git 事故。最后再分享一个压箱底的小技巧不管用 merge 还是 rebase动手之前先建一个备份分支两秒成本却能让所有操作都变成可撤销的。Git 从来不是靠背命令就能用好的工具真正需要的是理解它到底如何保存和串联历史理解了底层逻辑那些看似复杂的命令选择自然就有了答案。
返回列表