news 2026/9/7 17:24:11

Git冲突解决实战:从理解合并本质到从容处理代码分歧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Git冲突解决实战:从理解合并本质到从容处理代码分歧

1. 先别急着学命令,把「冲突」这件事想明白

很多人一遇到 Git 冲突就条件反射地开始背命令,git merge --abortgit checkout --oursgit rebase --continue——仿佛冲突是个 Bug,只要命令用得够快,它就会消失。但我做了几年的代码管理和团队协作支持后,越来越确定一件事:冲突根本不是命令能解决的问题,而是理解力的问题。

1.1 冲突的本质:两个分支在同一时间对同一处代码做了不同的事

我用一句话来概括冲突的本质:当 Git 需要把两个分支的历史合并在一起时,发现两个分支都对同一个文件的同一个位置做了修改,而且修改内容不一样,Git 无法自行裁决谁是对的,于是它停下来,把这个「裁决权」交给你。

这个「停下来」不是 Git 坏了,也不是你的操作有误,而是 Git 在给你发送一个信号——请你看看当时发生了什么。很多人把冲突当成灾难,抱着逃难心态去应付,当然越弄越乱。换个角度看,冲突其实是版本管理系统的安全机制:与其让 Git 乱猜合并出一个错误结果,不如明确地提示你「这里需要人类判断」。

我常拿现实生活里的场景来类比——你和你室友合租,你们在一张便签上轮流记录今天买了什么。结果某天你俩同时下班回家,都在同一张便签的同一行写了「买了牛奶」,一个写的是「伊利」,一个写的是「蒙牛」。你让房东来判断该信谁,房东当然判断不了,只能把你们两个拉到一起,当面问清楚。Git 就是那个房东,冲突标记里的每一段内容,就是你们两个各说各话的证据。

1.2 为什么说冲突不是技巧问题,而是你是否理解「同时发生了什么」

我们拆一个最常见、也最典型的例子。你在main分支上开发,第 60 行写着:

def get_user_info(user_id): return {"name": "张三", "age": 18}

现在有两个分支从这一刻开始分道扬镳。

分支 A 的开发者觉得用户信息需要加一个字段,于是把第 61 行改成:

def get_user_info(user_id): return {"name": "张三", "age": 18, "city": "北京"}

分支 B 的开发者觉得age这个字段涉及用户隐私,应该删掉,于是把同一行改成了:

def get_user_info(user_id): return {"name": "张三"}

这两个改动都发生在同一个位置,而且都是基于同一个原始版本改的。当你把分支 B 合并进分支 A 时,Git 就卡住了——它不知道该保留city,还是该删掉age,还是两者兼顾。Git 不理解的不是变量的含义,而是两个开发者各自的意图。

你只有把两个分支各自的「上下文」还原出来,才能真正解决冲突。比如你去看分支 A 的 commit message,发现写的是「用户资料页增加城市展示」;再去看分支 B 的 commit message,写的是「移除年龄字段以符合隐私规范」。当你理解了这两个背景,你就会知道:B 分支删除age是产品层面的决策,和 A 分支新增city并不矛盾——所以正确的解法是保留city,同时删掉age,最终结果应该是{"name": "张三", "city": "北京"}

这才是解决冲突的正确路径。它靠的不是某个冷门命令,而是你对业务背景、对两个分支「同时发生了什么」的还原。命令只是最后落笔的工具,理解才是决策的依据。

2. git merge 冲突的完整实战拆解

2.1 最小化复现一个冲突场景

理解归理解,但真要操作起来,很多人还是会被冲突标记里的符号绕晕。我建议你亲手复现一次冲突,把「冲突到底长什么样」刻进脑子里,以后再遇到就不会慌。

在你的工作目录下新建一个演示仓库:

mkdir git-conflict-demo cd git-conflict-demo git init git config user.name "demo" git config user.email "demo@example.com"

创建初始文件app.py

def greet(name): return f"Hello, {name}!" if __name__ == "__main__": print(greet("World"))

提交初始版本:

git add app.py git commit -m "initial commit"

接着创建两个分支:

git branch feature-a git branch feature-b

切换到feature-a,修改greet函数,加一个语气词:

git checkout feature-a

把文件改成:

def greet(name): return f"Hello, dear {name}!"

提交:

git commit -am "feature-a: improve greeting"

再切换到feature-b,同样修改greet函数,但改成更正式的欢迎语:

git checkout feature-b

把文件改成:

def greet(name): return f"Good morning, {name}!"

提交:

git commit -am "feature-b: use morning greeting"

现在你切回feature-a,尝试把feature-b合并进来:

git checkout feature-a git merge feature-b

你会看到类似这样的输出:

Auto-merging app.py CONFLICT (content): Merge conflict in app.py Automatic merge failed; fix conflicts and then commit the result.

打开app.py,你会看到下面这个经典结构:

def greet(name): <<<<<<< HEAD return f"Hello, dear {name}!" ======= return f"Good morning, {name}!" >>>>>>> feature-b

这就是冲突现场。整个过程可以复现,说明冲突不是随机发生的,它是 Git 在特定条件下必然触发的状态机。理解了这一点,你就掌握了主动权。

2.2 冲突标记的详细解读:<<<<<<< ======= >>>>>>> 到底在说什么

刚接触 Git 的人看到这一堆尖括号通常会有点懵。我来一行一行拆给你看。

<<<<<<< HEAD表示从这里开始,是当前分支(也就是你执行git merge时所在的分支)的内容。注意HEAD这个关键字指的是当前分支的最新提交,不是某个具体的分支名。

=======是分隔线,它把「当前分支的内容」和「被合并分支的内容」隔开。

>>>>>>> feature-b表示到这里结束,上面一段是被合并分支feature-b)的内容。

所以在上面那个例子里,<<<<<<< HEAD=======之间的Hello, dear {name}!feature-a的版本,=======>>>>>>> feature-b之间的Good morning, {name}!feature-b的版本。

这三行标记本身不是代码,你必须在解决冲突后把它们删掉。很多新手在完成修改后直接git add提交,结果把<<<<<<<=======>>>>>>>这些符号也带进了仓库,不仅编译报错,后来的人看着代码一头雾水。我自己就接手过这种「历史遗留冲突标记」,排查了半天才发现是有人没清理干净。

判断是否清理干净,最简单的方式是在解决完冲突后搜索整个项目里是否还有冲突标记:

grep -rn "^<<<<<<<\|^=======\|^>>>>>>>" .

如果有输出,说明还有残留;没有输出,才算真正的解决。

2.3 解决冲突的两种思维:保留一方 vs 合并双方

面对冲突内容,大部分情况的解决方案可以归为两类。

第一类是「明确保留一方」。比如上面例子里,你和同事讨论后决定使用feature-bGood morning, {name}!,那你要做的就是删除<<<<<<< HEAD=======以及属于feature-a的那一行,只保留Good morning, {name}!>>>>>>> feature-b下面保留的代码,最后把>>>>>>> feature-b也删掉。最终文件看起来应该像这样:

def greet(name): return f"Good morning, {name}!"

在 Git 里,git checkout --oursgit checkout --theirs可以快速实现「保留一方」,但我建议你只在目标明确时才用。因为--ours--theirs在 merge 和 rebase 场景下的含义是反过来的,稍不留神就选反了。在 merge 时,--ours指当前分支,--theirs指被合并的分支;但在 rebase 时,--ours指向的是目标分支,--theirs指向的是你自己的提交。这里绕来绕去特别容易出错,所以我的建议是:除非你非常清楚当前操作是什么类型,否则老老实实手动编辑文件。

第二类思维是「合并双方」。这个更考验你对业务的理解。回到我们最初举的例子,如果分支 A 加了city,分支 B 删了age,这两个改动互不冲突且目标不矛盾,那你应该把两边的改动都保留下来,拼出一个完整的新版本。

手动编辑时,合并双方需要你仔细对照冲突标记两边的代码,判断哪些是兼容的,哪些是相互排斥的。如果两边的改动恰好涉及同一行,你需要把两个改动融合到同一行里。比如:

<<<<<<< HEAD return {"name": "张三", "age": 18, "city": "北京"} ======= return {"name": "张三"} >>>>>>> feature-b

最终应该合并成:

return {"name": "张三", "city": "北京"}

age删除(采纳 B 的意图),把city保留(采纳 A 的意图)。做完之后,记得把文件保存,然后进行下一步。

3. 进阶场景:rebase 冲突为什么更让人头大

3.1 rebase 和 merge 冲突的核心区别

网上有一种说法叫「能用 rebase 就别用 merge」,理由是 rebase 能保持提交历史干净整洁。但对冲突解决来说,rebase 的难度通常比 merge 大一个量级。这不是命令难,而是它的运作方式和人的直觉不一样。

git merge是「把两个分支的历史合并成一个节点」,它只要在最终节点上解决一次冲突就够了。而git rebase是「把你当前分支的提交一个个摘下来,重新应用到目标分支之上」,相当于把你分支上的每一个 commit 都当成一次新的改动,逐个应用到目标分支的最新代码上。如果你分支上有 5 个提交,且都和目标分支的代码产生了冲突,那么你要依次解决 5 次冲突,而不是 1 次。

更要命的是,rebase 过程中每解决完一个冲突,状态会往前推进一步,但你可能已经记不清自己刚才改的是什么了。所以很多人对 rebase 冲突的恐惧,本质上是对「状态推进」的恐惧——你不仅要从代码角度理解冲突,还要从时间线角度理解冲突。

我用一个场景来说明。假设main分支上有一个文件config.py,原始内容是:

DEBUG = True

main分支后来有人提交了:

DEBUG = False ENV = "production"

你的feature分支基于旧的main创建,你也修改了config.py

DEBUG = True FEATURE_FLAG = "enabled"

现在你执行:

git checkout feature git rebase main

Git 会把你的提交摘下来,试图应用到现在已经变成DEBUG = False / ENV = "production"main上。你的提交里改了第一行的DEBUG,而main也改了第一行,于是冲突出现:

<<<<<<< HEAD DEBUG = False ENV = "production" ======= DEBUG = True FEATURE_FLAG = "enabled" >>>>>>> feature (your commit message)

注意了,HEAD此时指向的是main(因为 rebase 把你放在了main的基础上),而>>>>>>>后面写的才是你自己的提交。这和 merge 时的方向是正好反过来的,新手在这里几乎必踩坑。

这里解决冲突更需要理解「同时发生了什么」:main上把DEBUG改成False并增加了ENV,是因为生产环境的需求;你的分支上加了FEATURE_FLAG,是一个独立的功能开关。合理的合并结果应该是:

DEBUG = False ENV = "production" FEATURE_FLAG = "enabled"

3.2 rebase 冲突的实操要点

如果你确实需要用 rebase,并且遇到了冲突,请牢记以下几个操作要点。

第一,不要慌着连续解决多个冲突。Git 会引导你一个一个来,每解决一个冲突后执行:

git add config.py git rebase --continue

如果接下来又出现新的冲突,就继续解决、继续git add、继续--continue。直到所有提交都被重新应用完,rebase 才算完成。

第二,如果中途发现情况失控,想回到起点,可以执行

git rebase --abort

这个命令会撤销整个 rebase 过程,将分支恢复到执行 rebase 之前的状态。我这里明确地说:--abort是你的安全网,但不要指望每次都能靠它兜底。如果你已经执行了git add--continue,再想退回去就没那么轻松了,所以想确认之前,别轻易决定。

第三,单个提交内部的冲突,解决思路和 merge 一致,但要额外注意每个提交的独立性。你解决第二个冲突时,应该基于「第一个冲突解决后」的状态来思考,而不是基于原始状态。这就意味着,如果前面的冲突选错了方向,后面的冲突很可能跟着错。所以,如果同一批提交里出现多次冲突,建议你每解决完一个就停下来,看看整体上下文是否还合理。

我自己的习惯是:在 rebase 之前先给当前分支打个备份标签。这样做的好处是即便--abort失败,我也可以手动回到之前的状态:

git branch backup-feature-before-rebase git rebase main

如果 rebase 过程顺利,事后删除备份分支即可:

git branch -D backup-feature-before-rebase

别嫌麻烦,这一步是真正的保险。

4. 更多冲突场景:cherry-pick、stash pop 和 pull

4.1 cherry-pick 冲突

很多人只知道 merge 和 rebase 会冲突,却忽略了git cherry-pick也会。cherry-pick的作用是把某一个提交从别的分支复制到当前分支。它本质上也是「把你的改动应用到另一段历史上」,所以只要被复制的提交和你当前分支的对应代码有冲突,Git 同样会停下来。

场景举例:你在main分支上有一个提交修复了一个紧急 Bug,提交号是abc1234。现在你想把同样的修复同步到release分支,执行:

git checkout release git cherry-pick abc1234

如果release分支上对应位置的代码和abc1234的改动重叠,你同样会看到冲突提示。此时解决冲突的写法和 merge 基本一致,手动编辑文件,然后:

git add 文件路径 git cherry-pick --continue

如果你决定放弃这次 cherry-pick:

git cherry-pick --abort

有一点要注意:cherry-pick会产生一个新的提交,但这个提交的内容是你解决冲突后的结果,而不是原始提交的原样复制。所以 cherry-pick 之后要重新跑一遍测试,别以为「这个提交在别的分支已经测过了」。

4.2 stash pop 冲突

git stash是临时保存工作区改动的命令,通常配合git stash pop恢复。它看起来不太像一个会产生冲突的操作,但如果你在 stash 之后又对同一个文件做了别的改动,恢复 stash 时就有可能出现冲突。

我遇到过这样的实际场景:我在feature分支上改了login.js,临时 stash 之后切到main分支修复了一个线上问题,也改了login.js。修复完切回feature,执行git stash pop,结果提示冲突。

这里的原因也很好理解:stash pop本质上是把你保存的改动和当前工作区的状态进行合并。只要两边改到了同一个位置,冲突就无法避免。解决方案同样是手动编辑冲突文件,然后:

git add 文件路径

注意,stash pop没有对应的--continue命令,你解决完冲突后,直接正常提交就可以了。如果你在stash pop之后想反悔,可以执行:

git checkout -- 文件路径 git stash list

但如果你已经提交了,再想回到 stash 之前的状态就比较麻烦了。所以建议大家在做可能引发冲突的操作之前,先想好退路。

4.3 pull 的默认冲突与 merge 模式

还有一个隐藏较深的场景:git pull。很多团队习惯每天都git pull拉最新代码,但如果你的本地分支落后于远程分支,且有未推送的提交,pull就会触发一次隐式的 merge。如果远程代码和你的本地提交改到了同一个文件,冲突就会直接出现。

这个冲突解决起来和普通的 merge 没有区别,唯一要强调的是:别把git pull当成一个「只读操作」。它会改变你本地仓库的历史状态,甚至会进入一个临时的 merge 状态。如果你不希望每次 pull 都隐式 merge,可以改用:

git pull --rebase

这样会把你本地的未推送提交 rebase 到远程分支之上,历史更干净,但正如我们前面所说,rebase 冲突的解决难度更高。到底用哪种模式,取决于团队约定,没有绝对的对错。

5. 工具选型:命令行、可视化工具和 IDE 怎么选

5.1 命令行解决冲突的优缺点

命令行解决冲突的优势是通用、可脚本化、不依赖 GUI 环境。无论在本地终端还是 CI 服务器上,命令行的交互方式一致。它的缺点是可视化程度低,面对多个文件的复杂冲突时,你很难快速形成全局观。尤其是涉及重命名、文件移动这类结构性变化时,纯命令行会让你有种「盲人摸象」的感觉。

我自己在命令行下解决冲突,一般会配合几个辅助命令来快速掌握全局状况:

git status

这个命令会列出所有存在冲突的文件,Unmerged paths一类就是你当前需要处理的目标。

git diff

不加参数时,它只会展示已暂存但尚未提交的改动,但在冲突状态下,git diff会直接显示冲突区块的详细差异,帮助确认两边各自的版本。

如果你只有少数几个小冲突,命令行完全够用。我见过很多老程序员就用一个原生终端解决所有冲突,因为他们对代码的理解足够深,不需要可视化辅助。

5.2 可视化工具:VSCode、Beyond Compare 和内置 mergetool

如果你的冲突数量多、涉及面广,可视化工具的价值就很明显了。它们能把冲突的「两个版本 + 合并结果」并排展示出来,让你一目了然地看到哪个改动属于哪个分支。

我用得比较多的是 VSCode 内置的合并编辑器。当出现冲突时,VSCode 会在冲突区块上方提供按钮,你可以选择「Accept Current Change」「Accept Incoming Change」「Accept Both Changes」,还能直接进入手动编辑模式。对于习惯在 VS Code 里写代码的开发者来说,这个流程是最顺滑的。

如果你需要更专业的逐行对比,还可以配置Beyond CompareKDiff3作为 Git 的默认合并工具:

git config --global merge.tool bc3 git config --global mergetool.bc3.path "C:/Program Files/Beyond Compare 4/BComp.exe"

然后在冲突状态下执行:

git mergetool

Git 会依次打开每个冲突文件,调用可视化工具让你编辑,保存后它会询问你是否已经解决。这种模式的优点是可以精确控制每一行的去留,对比信息完整;缺点是外部工具的配置门槛稍高,新手容易卡在路径配置上。

关于工具选择,我的建议是:分清场合。小冲突、少量文件的冲突,用命令行加手动编辑器足够;大型重构、几十个文件冲突,或者你面对不太熟悉的模块时,务必用可视化工具辅助。工具的作用是帮你更清晰地理解冲突,而不是替你做决策。

6. 如何从根源上减少冲突

6.1 经常同步主干,小步提交

冲突之所以会堆积,往往不是因为某一方改得多、改得猛,而是因为双方偏离主干的时间太长。两个分支长期不合并,各自积累了大量改动,一旦合并,冲突范围就会特别大,解决起来费时费力。

一种有效的做法是「小步快跑」。你的特性分支不要埋头写好几天才合并一次,而是每天至少从主干同步一次:

git fetch origin git merge origin/main

或者用 rebase 方式保持历史线性:

git fetch origin git rebase origin/main

同步得越频繁,主干上的变化越少,冲突出现的概率越低,即使出现冲突,范围也小得多。这就像你每天整理一次桌面,和三个月不整理的结果完全不一样。

6.2 模块化开发,减少同一文件的竞争

冲突的本质是多人改了同一处代码,那么减少冲突的根本手段之一,就是让代码结构本身鼓励分工。

如果一个团队的人都往一个巨大的utils.py文件里塞函数,那么这个文件成为冲突高发区几乎是必然的。合理的做法是拆分模块,让每个人的改动尽量落在不同的文件里。比如把工具函数拆成string_utils.pydate_utils.pyfile_utils.py,每个人改动自己的模块,冲突自然减少。

这个思路严格来说不是 Git 的使用技巧,而是工程管理层面的预防策略。但很多团队恰恰忽略了这个层面,只知道出了问题再去解决,效率肯定低。

6.3 避免格式化混用:代码风格统一的隐形成本

一个常被忽略的冲突来源是格式化差异。比如你用的是 4 空格缩进,同事用的是 2 空格缩进;或者你用的编辑器会自动把行尾从 LF 改成 CRLF。这些看似「不重要」的差异,一旦遇上自动格式化,会在整个文件范围内产生海量 diff,导致两个人其实只改了几行代码,却在冲突标记里看到整个文件都「变了」。

因此,团队层面的格式化统一极其重要。比较靠谱的方案是使用.editorconfig或团队的 Prettier/Black 配置,让所有成员的编辑器都按同一种规则处理缩进、引号、行尾等细节。另外,仓库里要有一个明确的.gitattributes配置,避免文件行尾在不同平台间被自动切换。我在一个项目里就是因为行尾符问题,经历过一次「无意义的全文件冲突」,后来加上了统一配置,这个问题就再没出现过。

7. 常见错误与避坑经验

7.1 提交了残留的冲突标记

这是我见过最多的新手错误。手动解决冲突时,有人会把冲突标记看成代码的一部分,或者删了一半就保存并提交,导致仓库里残留<<<<<<<=======>>>>>>>。这些问题对代码的破坏是灾难性的。

有一个简单的规避方法:提交前做一个残留标记扫描。在项目根目录执行:

grep -rn '^<<<<<<<\|^=======\|^>>>>>>>' --include="*.py" --include="*.js" --include="*.java" .

如果没有输出,再进行提交。如果团队已有 CI,也可以在 CI 里加上这条检查,拦截这类问题。

7.2 分不清 ours 和 theirs,解决反了

前面说过,在合并和变基的不同场景下,ourstheirs指代的分支是相反的。这个坑不仅坑新手,有时候工作多年的老手也会在 rebase 时一时迷糊。

规避方式很简单:解决冲突时,打开冲突文件,看<<<<<<<后面的标签。如果是HEAD,那这段是当前分支的内容;如果是分支名或 commit hash,那这段是被应用内容。基于这个判断来决定怎么改,而不是想当然地使用--ours--theirs

7.3 过早提交,导致无法继续 merge

解决冲突后正常提交本身没错,但有一种情况要注意:你正在执行git rebase,结果解决完第一个冲突后直接执行了git commit而不是git rebase --continue。这样做会让 rebase 的状态变得混乱,Git 可能会提示你继续 rebase 或处理其他问题。

所以请记住:**merge 冲突解决后,用git add+git commit;rebase 冲突解决后,用git add+git rebase --continue;cherry-pick 冲突解决后,用git add+git cherry-pick --continue。**不同操作对应的收尾命令不同,混淆了就很麻烦。

7.4 不做备份,回不了头

这里我不只针对新手,老手也容易在操作中犯「来不及备份」的错误。尤其是一些破坏性操作,比如git reset --hard搭配冲突解决、git branch -D等,稍有不慎就可能丢掉工作成果。

稳妥的做法是在进行任何可能产生冲突或改动历史的大型操作前,先创建备份分支或使用git stash保存当前状态。做一次备份只需要几秒,但能避免大量的「找回代码」时间。

8. 一些个人经验和最后的建议

回到文章的题目——「不是技巧问题,而是你是否理解『同时发生了什么』」。这句话是我在带一个新人解决他的第一次冲突时总结出来的。他拿着冲突文件来找我,说不知道该选哪边,我让他别急着选,先把两个分支各自的 commit 看一遍。他看完之后一下就明白了,原来一边是需求方要求新增的功能,另一边是性能优化时删掉的冗余代码,两者根本不冲突,合并起来就行。

那次之后我意识到,Git 冲突的真正难点不在操作,而在上下文。你在解决冲突时,本质上是在做一次「代码考古」——把两个分支各自的历史、意图、改动量还原出来,站在全局视角做判断。这才是冲突解决的元能力。

最后分享一个小经验:如果你第一次处理一个模块的冲突,一定要去读那个模块的设计文档或者看看相关测试用例。很多时候,冲突里两个改动的取舍,代码本身不会告诉你答案,但测试会告诉你哪个结果是符合预期的。做一次充分的理解,比多敲十条命令更值钱。

希望你看完这篇文章之后,再遇到冲突时能少一分慌张,多一分从容。下次打开那个写满<<<<<<<的文件时,先问自己一句:这两个分支,究竟同时发生了什么?答案一旦清晰,命令怎么写其实都是顺理成章的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 17:23:32

LangChain多智能体实战:用LangGraph编排Agent构建婚礼策划系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:23:23

FlagOS深度解析:一套开源软件栈如何打通异构算力算子库

算力荒折腾了两年&#xff0c;我现在最深的体会是&#xff1a;大模型训练和部署的瓶颈早就不单是卡本身&#xff0c;而是软件栈被各家芯片厂商牢牢锁死。A卡一个生态&#xff0c;B卡一个生态&#xff0c;C卡又是一个半成品生态&#xff0c;每换一批卡就要把框架、算子、通信库重…

作者头像 李华
网站建设 2026/9/7 17:21:52

AI辅助FPGA开发实测:从Vivado报错到RTL生成的边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 17:21:48

Stable Diffusion与LoRA模型实现AI角色随机生成技术详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华