资讯动态

Linux开发必备:从diff到git format-patch的补丁整理实战指南

发布时间:2026/10/21 15:57:04 来源:尧图企业网站定制
干Linux开发的尤其是做嵌入式、内核移植、BSP适配的几乎没有不跟代码差异整理打交道的。别人交付一个功能给你的往往不是一个完整工程而是一个patch你要参与协作也不是把整个目录打包发出去而是把改动整理成patch提交。这里面的关键点在于“差异整理”四个字而不是简单地把新旧文件放一起对比。整理差异看起来是小事但patch的生成、命名、拆分、校验、应用和回退每一环都有门道。这篇东西不讲虚的全部围绕如何把代码差异化整理成合格的patch从命令行diff的基本功到git format-patch工作流再到实际应用和冲突解决按我平时干活的顺序来写适合刚接触Linux开发、需要给同事或社区提交改动的朋友参考也适合那些已经在用patch但经常被冲突和格式问题搞到头疼的人。1. diff与patch的核心概念与场景认知1.1 差异整理的本质不是发代码而是发改动先理清一个概念。diff命令做的事情是把两个文件或者两个目录逐行比较输出它们之间的差异patch命令做的事情则是把这份差异应用到另一个文件上让目标文件变成你期望的样子。这两个命令一前一后构成了Linux下代码流转的标准方式。为什么要用这种方式而不是直接发整个文件有两个现实原因。第一是省流量、省review时间。一个内核工程动辄几万个文件你改了三五行代码如果发给对方一个完整文件对方还要自己猜你改了哪里如果发patch评审的人只需要看patch里的改动块五分钟就能搞清楚意图。第二是版本管理上的可追溯性。patch天生自带上下文它记录了“在什么位置、基于什么内容、改成了什么”。我的理解是patch其实像装修时给工人看的“改动图纸”而不是把整栋楼推倒重盖。图纸上只标哪里要敲、哪里要砌、用什么材料工人拿到图纸就知道怎么施工。代码patch同理它只承载“变化量”不承载“全量”。1.2 不同场景下的补丁形态与选择补丁不是只有一种玩法。根据场景和代码规模常见的有这么几类补丁形态生成方式适用场景单文件补丁diff -u old.c new.c快速修复一个接口函数目录级补丁diff -Naur src_old/ src_new/新增目录、批量修改文件Git工作区补丁git diff未提交的本地改动提交级补丁git format-patch -N需要保留提交信息、多提交序列跨版本适配补丁patch 手动调整把新版本补丁移植到旧版本代码选哪种形式不重要重要的是先想清楚接收方需要什么。如果对方只需要你的改动内容手写diff就够如果对方还需要知道改动历史、提交原因那就要用git format-patch。单文件补丁一般就是两个人对着同一个文件改或者你确认只改了一个源文件目录级补丁多用于整个模块的增删比如把一个驱动的几层文件从旧版本升级到新版本提交级补丁适合团队协作因为它不单带代码差异还带提交说明、作者署名、评审信息。提示不要一上来就想用什么高级工具先明确场景和接收方的使用习惯。你花十分钟整理好一个patch可能为对方省下一下午的排查时间。2. 基础diff操作与补丁文件整理2.1 单文件对比diff -u 与补丁头解读先从最基础的单文件说起。假设你有两个文件一个叫main_old.c一个叫main_new.c你想知道新文件相对于旧文件改了什么最简单的命令是diff -u main_old.c main_new.c-u的意思是输出unified格式也就是统一格式。它会把旧文件和新文件的差异集中显示在一起而不是像默认格式那样分开成两段。统一格式是当前主流补丁格式因为它上下文清晰patch命令解析也最稳定。输出长什么样以一段最简单的为例--- main_old.c 2025-01-01 10:00:00 main_new.c 2025-01-01 12:00:00 -20,6 20,8 void init_module(void) { /* setup */ /* add new feature */ register_ops(); }这里每一行都要会看。---开头的是旧文件开头的是新文件它们后面跟的是文件名和时间戳这是定位补丁基线用的。 -20,6 20,8 表示旧文件从第20行开始有6行新文件从第20行开始变成了8行。为什么多了两行因为下面以号开头的两行就是新增内容。带空格开头的行是公共上下文patch在应用的时候会拿这些上下文到目标文件里去匹配位置。一个容易被忽略的细节是-U参数它控制上下文行数。比如diff -u -U5 main_old.c main_new.c这个命令让每个差异块上下各保留5行公共内容。上下文越多patch在目标文件里定位越准但补丁体积也越大。我一般用默认的3行遇到目标文件可能被改动过、容易错位的情况我会把上下文调到5到6行。2.2 目录级对比-Naur参数全解单文件diff只是热身实际工作中更常见的是整个目录级别的对比。比如你有一份原始代码目录src_ori一份自己改过的目录src_mod需要把整个目录的改动整理成一个patch核心命令是diff -Naur src_ori src_mod my_changes.patch这四个参数一个都不能少。N表示把新增文件当作全新增量处理。什么意思不带N的话如果src_mod里多出来一个文件diff默认不会把它输出到patch里因为diff认为新文件不存在对应旧文件没得比。加上N之后diff会把新文件的所有内容都以号形式写进patch应用patch时就能创建这个文件。反过来如果旧文件在mod目录里被删除了不带N也不会反映而带N后patch里会以删除整个文件的方式标记出来。a表示把一切文件都按文本文件处理。不加这个参数时diff遇到二进制文件会直接提示“Binary files differ”但不会把差异写进patch。如果你的代码树里有一些二进制配置或者固件文件需要一起打包就必须加a。如果不想包含二进制就别加a保持默认反而更安全。r表示递归对比子目录这个不用多说目录有嵌套结构就必须递归。u就是前面说的unified格式。这条命令是手工出patch的传统主力很多老工程师都是用它。它的好处是不依赖Git任何代码目录都能比坏处是你必须在动手改代码之前留有原始代码的副本否则没有对比基线。有人问那我没有原始副本怎么办如果代码是在Git仓库里可以用git diff它天然知道基线版本如果不在Git仓库我建议以后开工前先cp一份backup或者直接初始化一个Git仓库成本很低收益很高。2.3 排除噪音文件与补丁归档规范目录大对比有一个常见问题噪音文件太多。所谓噪音文件包括编译生成的.o、.ko、.bin编辑器产生的临时文件如xxx.c~、#xxx.c#还有自动生成的依赖文件.d、.cmd等等。如果这些文件混进patch轻则补丁体积虚胖、review人吐槽重则因为路径或二进制问题导致patch应用失败。解决办法就是给diff加排除规则diff -Naur --exclude*.o --exclude*.ko --exclude*.bin \ --exclude*.d --exclude*.cmd --exclude*.tmp \ --exclude*~ --exclude*.orig --exclude*.rej \ src_ori src_mod my_changes.patch--exclude后面跟的是通配符或目录名可以重复使用。如果排除规则太多每次敲一遍很烦可以写成一个脚本或者把公共规则放到脚本变量里。补丁文件本身的归档也要讲规矩。我见过太多人拿着一个叫“1.patch”或者“新建文本文档.patch”的文件发出来接收方拿到手一脸懵。一个规范的补丁文件名至少应该包含日期、模块、改动意图三段信息例如20250115_driver_net_fix_reset.patch。这样即使过了半年翻出来还能一眼看出这patch是干什么的。如果是多个补丁组成一个序列我建议再加一个编号比如001_fix_typo.patch、002_enable_dma.patch顺序就是应用顺序。同时建议在补丁开头用注释或者单独的说明文件写明基线版本和改动要点。所谓“整理”不只是diff输出那一下而是从文件命名到内容组织的一条龙。3. 基于Git的补丁整理工作流3.1 git diff生成补丁基线管理更省心如果代码已经在Git仓库里我强烈建议直接用Git工作流而不是拿着diff原始目录去搞。原因很简单Git帮你管理了“对比基线”。你在一份干净代码上做修改改动之后只需要敲git diff my_changes.patchGit会自动拿当前工作区和暂存区或HEAD对比生成和diff -u同格式的输出。如果一个文件是新加的git diff默认不包含未跟踪文件所以你需要先用git add把新文件加进暂存区再执行git add new_file.c git diff --cached my_changes.patch这条命令生成的是“当前暂存区相对于HEAD”的差异。如果你既要包含已暂存的也要包含未暂存的可以分两步生成再合并或者用git diff HEAD直接对比工作区与HEAD之间的总差异。我实际用得比较多的是git diff HEAD因为我要保证patch里包含所有对已有文件的修改。对于新文件一定记得先git add因为它从“未跟踪”变成“已暂存”状态后git diff HEAD才会把它算进差异里。3.2 format-patch与am带提交信息的补丁流转git diff生成的patch只包含代码差异不包含提交信息。如果是个人之间传代码这勉强够用但如果是团队协作、代码评审、或者跨分支移植一个功能强烈建议用git format-patch。# 把最近2个提交分别生成独立patch git format-patch -2 -o /tmp/patches/ # 把指定提交之后的改动生成patch不包含指定提交本身 git format-patch commit_id..HEAD -o /tmp/patches/format-patch的输出不是单个patch而是每个提交一个.patch文件文件名形如“0001-module-name.patch”。这个patch和git diff的patch有一个重大区别它在头部嵌入了完整的提交信息包括提交标题、说明、作者、日期。接收方用git am应用时不但能把代码差异打上去还能直接生成一条包含原提交信息的Git提交记录作者信息也能保留下来。这就解决了协作中的元数据问题。以前用diff补丁应用后提交信息是接收方自己重新写的写着写着就和原意图有偏差format-patch加git am提交信息原样保留评审、追责、回溯都清晰。对应地接收方应用时用git am /tmp/patches/0001-module-name.patchgit am比起patch命令更严格它会先做预检测如果补丁不能干净地应用会停下来等你处理并不像patch命令那样尝试模糊匹配、产生.rej文件。这是好事因为它杜绝了“补丁虽然打上了但位置完全错了”的隐患。3.3 提交拆分、补丁序列与rebase技巧工作流里最需要“整理”的环节是一个功能涉及多个文件改动混在一起但逻辑上应该拆成几个独立提交。比如你修了一个bug的同时加了一组调试日志这两件事相关性很低如果混在一个patch里评审人很难判断后续想单独回退某一部分也困难。解决办法是交互式暂存git add -p这个命令会打开一个交互界面逐个hunk让你选择是否暂存。你可以按y暂存当前块按n跳过按s把这个块拆成更小的块按e手动编辑块的边界。配合提交git add -p git commit -m fix: 修复某某接口越界问题 git add -p git commit -m debug: 增加发送路径的日志输出这样两件事就被拆成了两个提交。之后再用format-patch导出的就是两个逻辑独立的patch。提交顺序不满意怎么办用rebase交互模式git rebase -i HEAD~3编辑器里会列出最近3个提交你可以调整顺序、合并squash多个提交为一个、修改提交说明。我在整理补丁序列时习惯先保证每个提交只做一件事再保证提交之间的顺序是按照依赖关系排的基础功能在前依赖基础功能的增强在后。如果对方只想要一个总patch不想看提交序列可以直接用参数合并git format-patch --stdout base..HEAD total.patch这样生成的其实是一个包含全部差异的单文件patch可以用git apply或patch命令应用。4. patch应用、校验与回退4.1 patch命令正确姿势p级路径和干跑拿到一个patch之后怎么安全地把代码应用上先讲传统patch命令。习惯用法patch -p1 my_changes.patch-p后面跟的数字表示去掉路径中的多少层目录。为什么会有这个参数因为patch文件内部记录的路径可能带有前缀目录比如a/kernel/sched/core.c和b/kernel/sched/core.c其中a/和b/就是diff默认产生的虚拟前缀并不存在于你的真实目录。如果当前你在内核源码根目录-p1就是去掉这种虚拟前缀让kernel/sched/core.c对应到真实文件-p0则是不去掉层级要求目标文件就在当前路径下。还是不太明白的话可以看patch第一行--- a/kernel/sched/core.c b/kernel/sched/core.c当前目录是内核根目录用-p1会去找kernel/sched/core.c。当前目录是kernel/用-p1会去找sched/core.c这通常不是你想要的。当前目录是内核根目录用-p0会去找a/kernel/sched/core.c也不对。所以打开patch查一下第一行的路径再决定-p几是应用patch前最重要的步骤。在真正应用之前强烈建议先干跑一遍也就是只检查不应用patch -p1 --dry-run my_changes.patch它能告诉你哪些文件能干净地打上、哪些会失败。确认没有FAILED之后再去掉--dry-run正式应用。批量打一堆patch时干跑尤其有用能避免打了一半才发现方向错了。4.2 应用失败、冲突与.rej文件机制即使干跑通过正式应用还是可能失败因为干跑只是静态检查应用过程也可能因为文件修改时间变化或者多个patch之间有依赖而失败。失败的典型表现是patch命令给出“FAILED”提示并在目标目录下生成一个同名但以.rej结尾的文件里面是被拒绝的差异块同时可能还生成.orig文件保存的是被patch修改之前的原始内容。遇到FAILED我的处理顺序是先看.rej文件确认是哪些块没有被匹配上。查看对应的源文件判断行号偏移了多少或者上下文被谁改了。如果只是行号偏移手动编辑目标文件把改动按.rej里的内容补进去。补完后删除.rej和.orig用diff再验证一遍结果。不要把.rej文件留在代码树里它们是垃圾后面如果再做目录级diff会把它们当噪音带进新patch。还有一个常见情况补丁以前已经被应用过了再次应用时会提示“Reversed (or previously applied) patch detected! Assume -R?”。这个意思是patch检测到目标文件的当前状态已经包含了该补丁的改动上下文呈现出“反向”的特征它怀疑你是不是想把补丁回退。如果确认只是重复应用果断选择退出别选R否则会把改动回退掉。4.3 版本差异下的补丁适配与回退操作把patch应用到一个和生成时基线不一致的代码版本上是最考验经验的场景。内核社区每年有大量补丁要移植到旧版本或者不同分支上行号几乎不可能对得上但patch有一个fuzz机制它会在目标文件里搜索上下文即使行号偏移也能尝试匹配匹配到的位置和原始位置可能差很多。fuzz级别默认不是一个固定值patch命令会在一定范围内尝试。如果匹配失败严重可以手工指定更高的容忍度但副作用是可能把改动放到错误的位置。所以我一般不建议上来就调高fuzz而是先看.rej人工判断这个改动是否适合当前版本。跨版本移植补丁的正确做法不是直接patch而是打开patch文件理解它改的是什么逻辑然后在目标版本里找到对应的代码位置手动应用。因为大版本之间函数名、结构体定义、调用方式可能都变了自动工具很难处理这种语义级变化。回退补丁反而简单。如果补丁已经成功应用想要撤销它只需要反向应用patch -p1 -R my_changes.patch-R表示反向应用patch会把号行删掉、把-号行加回去。在Git仓库里更推荐用git apply反向git apply -R my_changes.patch如果改动已经变成一次提交直接git revert也好它不但回退代码还生成一条反提交记录历史更清楚。5. 一套可落地的补丁整理流程与边界情况5.1 推荐流程从写代码到交付patch前面讲了很多命令这里把它们串成一套我日常工作用的流程照着做可以让出patch这件事从“碰运气”变成“开箱即用”。第一步拿到代码基线后先确认工作区的干净状态。如果代码在Git仓库里git status检查不在的话先cp一份原始树做备份名字里带日期或者版本号。第二步改代码。可以边改边议但建议一个逻辑对应一次commit而不是攒一大堆再拆分。如果一开始没养成习惯事后用git add -p补拆也可以。第三步生成patch。改动小用git diff或git diff HEAD涉及多个提交、需要带提交信息时用git format-patch。手工目录对比用diff -Naur记得加排除规则。第四步自己验证。先在干净环境里apply一遍或者用git apply --check / git am --check检查确认patch能无冲突应用。再把应用后的文件用diff对比原期望结果保证没打错位。第五步归档。按“日期_模块_描述”的格式命名patch如果是序列就加编号附上说明文档记录基线commit和改动要点。这套流程看起来啰嗦实际执行起来不超过十分钟但它能避免很多“patch发过去对方打不上”的尴尬。5.2 常见问题速查表现象原因处理方法patch应用提示Reversed detected补丁已经被应用过确认后选n退出别反向应用应用后多了.orig/.rej有差异块没匹配上按.rej手动补清理垃圾文件git apply报patch does not apply上下文或行号不一致先看报错位置必要时用patch命令或手改patch应用成功但代码不对fuzz匹配到了错误位置看.rej与上下文人工重放目录diff结果有大量编译产物没排除*.o等噪音文件加--exclude再生成打开patch中文乱码文件编码或换行符不一致统一UTF-8与LF检查CRLF补丁只跟部分文件相关改动混在不同提交里git add -p拆分后重新导出换行符的问题值得单独提一句Windows下编辑过的代码常带CRLFLinux下是LFdiff出来行尾可能都被当成差异。推荐打开文件时用dos2unix统一一下或者在Git配置core.autocrlf。我吃过这个亏整整一个patch因为行尾差异变得几乎不可读。5.3 值得一提的补丁管理工具如果项目长期维护大量patch手工管理diff文件会很痛苦。我自己在内核和BSP开发中除了Git也用过quilt这样的传统补丁管理工具。quilt的核心概念是一个补丁栈每个补丁对应一个文件你可以push、pop、refresh随时查看某层补丁在代码上的效果。它适合那种“源码保持只读、所有改动以补丁形式叠加”的工作模式比如在构建系统里用quilt series管理一系列patch构建时自动应用。Git本身也是强大的补丁管理器。使用quilt之前如果项目已经在Git下先用好Git的分支和rebase思路是类似的每个patch相当于一个提交commit之间的顺序就是应用顺序。我更建议新手先把Git工作流跑熟再用quilt去应对那些特别依赖patch序列的场景。工具是次要的核心还是对“差异”的敏感度这个patch的基线是什么、改动是否符合目标版本、应用之后有没有影响其它模块。搞清楚了这些用diff、用git、用quilt都只是顺手的事。最后分享一个我自己的习惯。每次出patch之前我都会把自己想象成接收方打开这个patch的人能不能只看patch内容就知道我做了什么、为什么做、应用到哪里。如果答案是否定的我就会去调整格式、补全说明或者拆分提交。补丁这种东西本质上是一种协作语言语言通不通顺直接关系到别人愿不愿意认真review你的代码。代码写得再好patch一团乱说服力也会大打折扣。所以别怕在整理patch上花时间这些时间最后都会以更少的来回沟通和更顺利的合入回报给你。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑