尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux diff与patch命令详解:从生成补丁到应用补丁的完整流程

Linux diff与patch命令详解:从生成补丁到应用补丁的完整流程 搞 Linux 这行几乎没人能绕开diff和patch这对组合。不管是给内核提交补丁、给某个开源项目修 bug还是自己在服务器上维护一堆配置文件本质上都是“对比新旧差异、把差异打包、再应用到别处”这一套逻辑。diff负责找出差异patch负责把差异打进去两个命令配合起来就是一条完整的补丁工作流。今天这篇就围绕这个主题把diff生成补丁、patch应用补丁的完整流程拆开讲清楚。适合刚接触 Linux 命令行的新手也适合平时在用但没系统整理过这套流程的人。我会从命令参数、补丁格式、路径处理讲到实际演练和排错经验尽量把每一步背后的原因也交代清楚而不是只给一串“照着敲就行”的命令。1. diff 与 patch 这套流程到底在解决什么问题1.1 为什么不用直接传整个文件先想一个场景你在维护一个几十万行的源码项目改了两三个文件每个文件改了几行。如果你想把这个改动交给别人最笨的办法是把整个项目压缩包发过去。但这样做问题很明显——包大、浪费时间、而且对方很难一眼看出你到底改了什么。补丁机制的核心思路是只传递变化不传递完整内容。diff生成的补丁文件里记录的是“哪一行被删了、哪一行被加了、哪一行保持不变作为上下文”。这个补丁文件通常只有几 KB哪怕你的项目是几百 MB 也没关系。对方拿到补丁后用patch命令就能把自己的副本改成和你一模一样的状态。这个思路放在今天看可能觉得理所当然但在没有版本控制系统的年代这就是协作开发的命脉。即便现在 Git 已经普及diff/patch这套流程依然没有被淘汰——很多内核开发、嵌入式移植、软件打包的场景里补丁文件仍然是最标准、最通用的交流格式。1.2 整个流程只有三步这套工作流总结下来就三个动作生成补丁、传输补丁、应用补丁。生成补丁是开发者在自己的工作目录里完成的传输补丁可以靠邮件、聊天工具或者挂在代码仓库里应用补丁是接收方在自己机器上做的。生成补丁diff -u 旧文件 新文件 修改.patch应用补丁patch -p1 修改.patch回退补丁patch -R -p1 修改.patch看起来简单但实际运用中涉及不少细节目录层级怎么处理、新增文件怎么识别、二进制文件怎么办、补丁应用失败怎么排查。这些我都会在后面展开讲。先把核心思想记住diff 是减法思维patch 是加法操作一个是“找出区别”一个是“应用区别”。2. diff 命令参数和输出格式拆解2.1 三种常见的 diff 输出格式diff命令有很多输出格式日常接触最多的有三种。如果你看过 Git 的提交记录其实你已经见过其中一种了。先说普通格式normal也就是不加任何格式参数时的默认输出。它用和来标识“旧文件里的内容”和“新文件里的内容”靠a、d、c这三个字母表示添加、删除、修改操作。这个格式信息密度低、还带着方向符号实际使用中基本只适合人眼简单对比两个小文件。再说上下文格式context加上-c参数启用。它会输出变化的行以及变化前后各若干行作为参照用!标记发生变化的行。这种格式比普通格式好读不少但体积大因为每个变化点都要重复输出一大段上下文。最后是合并格式unified加上-u参数启用。它把前后的上下文压缩到一个区域里删除的行用-开头新增的行用开头变化点用一个行来定位。今天的 Git diff、补丁文件基本上都用这个格式。它的体积小、可读性好、最重要的是patch应用起来最可靠。2.2 高频参数逐个说diff的参数非常多但实际工作中高频出现的就这几个我按使用频率排个序-u输出 unified 格式生成补丁时必加否则 patch 工具可能无法识别格式。-r递归对比目录。没有它diff对目录只会报“它们是目录”而不会深入对比内部文件。-N把不存在的文件当作空文件来处理。没有它新增文件或删除文件根本不会出现在 diff 结果里。生成补丁时不加-N对方打补丁后不会多出新文件也不会帮你删掉该删的文件。-a把所有文件当作文本文件处理。默认情况下遇到二进制文件diff会提示“binary files differ”有了-a它就会强行做逐字节文本比较但输出的内容可能是乱码。-x 模式排除匹配的文件或目录。比如-x *.log就能忽略所有日志文件。如果想排除整个目录用-x .git之类。--strip-trailing-cr忽略行尾的\r。如果你在两台系统之间倒腾文件比如 Windows 和 Linux行尾符不一致导致 diff 结果稀奇古怪这个参数能救命。常用组合是diff -uNr 旧目录 新目录 改动.patch。这个组合意味着递归遍历、输出 unified 格式、把新增文件当作从空文件开始的修改。这样打出来的补丁是完整可用的。2.3 补丁文件头里的数字到底什么意思很多人第一次看到diff -u的输出会蒙圈。这里用一个小例子说明。假设旧文件old.txt内容为apple banana cherry新文件new.txt内容为apple banana kiwi运行diff -u old.txt new.txt输出大致是--- old.txt 2024-01-01 12:00:00.000000000 0800 new.txt 2024-01-01 12:00:00.000000000 0800 -1,3 1,3 apple banana -cherry kiwi第一第二行是文件路径和时间戳---对应旧文件对应新文件。第三行 -1,3 1,3 是 hunk 的定位信息-1,3表示旧文件的第 1 行开始、连续 3 行1,3表示新文件的第 1 行开始、连续 3 行。后面的在同一行上的额外内容如果有是所在函数或上下文信息。3. 生成补丁的实操方法与路径处理3.1 单文件补丁的生成单文件补丁是最简单的情况。比如你改了系统里的一个配置文件想把改动保留下来或发给同事就这么写diff -u /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak nginx-config.patch等一下顺序别搞错。diff -u 旧文件 新文件旧文件在前新文件在后。如果你写反了生成的补丁内容会把新增行和删除行对调对方应用后会把新版改回旧版方向完全反了。我见过不止一次因为这个顺序问题导致补丁应用后代码状态不符合预期的重点是这类错误还不报错很难察觉。3.2 整个目录树的补丁生成一个项目通常不止一个文件改动。这时候直接对比两个目录一次性生成整棵树的补丁diff -uNr original/ modified/ project-update.patch这里original/是原始未改动的代码modified/是你改完之后的代码。输出会把所有差异文件都汇总到project-update.patch里。注意目录对比时如果某个文件在modified/里是新增的务必确保加了-N参数否则这个文件不会出现在补丁中。同理如果你删除了某个文件不加-N的话接收方也不会看到删除操作。还有一个细节对比目录时路径分隔符和斜杠。patch应用补丁时是通过补丁文件里的文件名来定位目标文件的所以生成补丁时的“起始目录名字”会影响对方应用的命令。比如diff -uNr original/ modified/生成的补丁里文件路径会写成original/xxx.c和modified/xxx.c。对方把这个补丁放到他项目的根目录下需要用patch -p1剥掉第一层目录名也就是original/。3.3 路径层级和 p 参数的对应关系patch的-p参数是用来“剥掉路径层数”的这个必须理解清楚。补丁文件里写着a/src/main.c和b/src/main.ca/、b/前缀常见于git diff生成的结果patch -p1的意思就是“忽略第一段路径”于是a/src/main.c变成了src/main.c然后拿这个相对路径去当前目录下找文件。如果补丁里的路径是original/src/main.c而你当前就在项目根目录下同样需要-p1来剥掉original/。如果补丁里直接就是src/main.c那就不需要剥层用patch -p0。如果补丁是在项目根目录的上一级生成的路径是两个目录的完整相对路径可能得-p2甚至更多。记住这条经验拿到任何补丁先打开看一眼文件路径再决定用-p几。老手也不是靠猜都是先看补丁内容再敲命令的。4. patch 打补丁全流程与关键参数4.1 先干跑一下不要直接应用patch支持“预测模式”也就是先模拟应用一遍不实际修改任何文件。这是我认为这套流程里最值得养成的习惯patch --dry-run -p1 project-update.patch如果这条命令输出的结果是“patching file xxx”并且没有FAILED字样说明补丁可以顺利应用。如果某个文件报错说 “cant find file to patch”那你就要检查路径层级或者当前目录是不是对了。干跑模式的意义在于它能把代价降到零。正式应用一旦失败工作区里会残留.rejreject拒绝应用文件运气不好还会改掉一半文件留给你一堆半成品。先干跑、后正式这个顺序能帮你挡掉 90% 的意外。4.2 正式应用、备份与回退正式应用只需去掉--dry-runpatch -p1 project-update.patch默认情况下patch处理每个文件时如果发生冲突会把无法匹配的行写入后缀为.rej的文件被修改前的原始内容则会存成.orig文件。这两个文件往往意味着“这个补丁不能直接应用你最好手动处理冲突”。如果应用完发现效果不对或者补丁方向搞反了可以用-R参数回退patch -R -p1 project-update.patch-R会把补丁里的和-对调相当于撤销这次修改。前提是你应用补丁后没有继续手动改动那些文件否则回退也会失败。还有一个实用参数是-b它在应用补丁前会自动备份被修改的文件文件名后面多一个.orig后缀。和默认生成的.orig作用类似但-b的逻辑更明确保留应用补丁前的状态方便出问题随时还原。注意我强烈建议在任何可能出乱子的项目上先cp -r 项目目录 项目目录.bak做一份完整备份。patch -b只能备份被修改的单个文件不能备份整个项目结构。4.3 patch 的其他高频参数除了-p和-R还有几个参数在特定场景下非常有用。-d 目录可以在执行patch前先切换到指定目录适合你人不在项目根目录但补丁里的路径是相对路径的情况。-E在应用补丁后删除空文件如果补丁里删除了某个文件的全部内容这个参数能保证文件本体也被移除。--fuzzN设置匹配行时的模糊度patch在找 hunk 位置时允许上下文有少量不匹配默认值是 2调大能让某些因为微小平移导致的失败变成成功但也有可能找错位置属于“有风险就撞运气”的方案不推荐新手动。5. 实战演练从配置文件修改到补丁应用5.1 场景设定假设你在维护一个小型 Web 服务服务器上有两份 Nginx 配置目录。一份是当前正在用的nginx_old/一份是你调优后的nginx_new/。两个目录里都有一堆.conf文件其中server.conf改了几个参数upstream.conf是新增的文件。现在要把这些改动从本机同步到另一台服务器上。目录结构大致是这样nginx_old/ nginx.conf server.conf mime.types nginx_new/ nginx.conf server.conf upstream.conf mime.types5.2 执行命令与结果解读第一步生成完整补丁diff -uNr nginx_old/ nginx_new/ nginx-tuning.patch第二步查看补丁内容确认关键路径cat nginx-tuning.patch文件里应该能看到类似这样的段落diff -uNr nginx_old/server.conf nginx_new/server.conf --- nginx_old/server.conf 2024-06-01 10:00:00.000000000 0800 nginx_new/server.conf 2024-06-02 15:30:00.000000000 0800 -20,7 20,7 server_name example.com; listen 80; - worker_connections 1024; worker_connections 2048; keepalive_timeout 65; gzip on; }upstream.conf因为是新增文件还会有一段从/dev/null到新文件的 diff可能长这样diff -uNr nginx_old/upstream.conf nginx_new/upstream.conf --- nginx_old/upstream.conf 1970-01-01 08:00:00.000000000 0800 nginx_new/upstream.conf 2024-06-02 15:30:00.000000000 0800 -0,0 1,12 upstream backend { server 10.0.0.1:8080 weight5; server 10.0.0.2:8080 weight3; }然后把这个补丁文件传到目标服务器上在项目根目录也就是nginx_old/所在的父目录执行patch --dry-run -p1 nginx-tuning.patch如果输出里有两行patching file nginx_old/server.conf和patching file nginx_old/upstream.conf没有FAILED就可以正式应用patch -p1 nginx-tuning.patch注意这里我用的是-p1因为补丁里的路径带着nginx_old/这个前缀而目标目录就叫nginx_old。如果你在nginx_old/目录里面执行就得用-p1剥掉nginx_old/这样它才能去找server.conf。5.3 验证应用结果应用完成后验一遍很重要。最直接的办法是再次执行 diff看差异是否已经消失diff -uNr nginx_old/ nginx_new/如果没有任何输出说明两个目录现在完全一致补丁应用成功。如果想更谨慎一点还可以检查有没有生成.rej或.orig残留文件find . -name *.rej -o -name *.orig没有任何输出就是干净状态。这个习惯我建议保持.rej文件不会自己消失留久了很容易被误提交造成下一代补丁的混乱。6. 常见问题与排查技巧实录6.1 遇到这些报错别慌打补丁失败的场景基本都在下面这个表里。逐个对照排查大部分都能解决。报错或现象可能原因解决办法cant find file to patch当前目录不对或-pN层级不对查看补丁头部的文件路径到正确的目录下用正确的-p参数Reversed (or previously applied) patch detected补丁方向反了或者已经应用过了检查diff参数顺序若已应用过无需再次执行FAILED: xxx上下文不匹配目标文件和生成补丁时的基准文件差异太大打开.rej详情手动比对合并补丁应用成功但文件内容不对上下文匹配到错误位置fuzz 过度查看 hunk确认定位信息必要时手动修改行尾符不一致Windows 换行\r\n与 Linux 换行\n混用用--strip-trailing-cr参数或统一换行符最常栽的是第一种。项目目录没对上、-p1误写成-p0、补丁文件是在另一个完全不同的仓库根目录下生成的都会导致cant find file to patch。排查方法很简单打开补丁顶部看后边写的路径是什么然后问自己“我现在所在的目录下能不能通过这个相对路径找到文件”。能就用-p0不能就剥一层再找。6.2 日常维护补丁的几个经验说几个我在实际中总结出来的习惯希望对你有帮助。第一补丁文件的命名尽量带上日期和功能描述比如20240602-nginx-worker-connections.patch。时间一长你就知道什么fix.patch、update.patch这种名字隔一个月就完全不认识了。补丁文件不是一个一次性的临时文件它可能要在多个环境里传播、归档、追溯名字就是它的门面。第二能用git diff的时候尽量用git diff。现在大部分项目都在 Git 仓库里你改了文件没提交git diff 改动.patch就能生成非常规范的补丁。Git 生成的补丁格式天然适合patch -p1应用路径前缀固定是a/和b/减少了很多路径上的麻烦。如果你不想带a/、b/前缀可以git diff --no-prefix 改动.patch这样对方可以用-p0更直接地应用。第三如果你提交的补丁是要给内核或者大型开源项目用的记得先跑一遍项目自带的检查脚本内核里就是scripts/checkpatch.pl。它能帮你检查补丁有没有格式问题、多余空白、长行之类的毛病。很多维护者非常在意这些细节补丁格式都不规范的话技术内容再好也容易被拒。第四不要轻易用--fuzz去强行应用一个本来匹配不上的补丁。模糊匹配的工作原理是忽略少量上下文错误、尽可能把 hunk 放到看似“差不多”的位置上。如果目标文件和补丁的基准版本差异比较大模糊匹配很容易把修改放到错误的函数甚至错误的文件区域里而且不报错。补丁应用后编译不报错不代表它真的放对了位置。宁可让补丁失败、手动解决冲突也不要靠调大 fuzz 来“赌一把”。第五二进制文件不适合走 diff/patch 这条流程。虽然diff -a能强行比较并生成补丁但二进制格式一旦有了细微变化生成的补丁基本上是一堆不可读的乱码体积也不一定小。二进制文件的更新老老实实直接传完整文件或者用rsync这类工具按块同步别硬套补丁流程。这套 diff/patch 的玩法并不复杂核心就是“差异即补丁、补丁即差异”。会用diff -uNr生成补丁会用patch -p1应用补丁再掌握--dry-run先试、-R回退、.rej排错这几个关键技巧日常工作里九成以上的补丁场景都能稳稳拿捏。理解它背后“先对比、再传输、后合并”的协作思维你以后遇到 Git、Gerrit、邮件列表里的各种补丁交互思路也会清晰很多。
返回列表