bigywater.tech
Git · 76

clean/smudge filter 与自定义 merge driver

.gitattributes 里的 filter 和 merge 属性可以改变 git 存取与合并文件的方式。这篇用一个脱敏密码的 clean 过滤器和一个取最大版本号的 merge driver,把 clean / smudge 何时触发、为什么要互逆、driver 收到什么参数逐个验证,再对比内置的 binary 与 union。

clean → blob
add 时工作区内容经过 clean 才存成 blob,工作区本身不变
%O %A %B
自定义 merge driver 的参数:基础、ours、theirs 的临时文件;结果写回 %A
union
内置 merge=union:冲突的两边新增的行都保留

clean 与 smudge

实测 · 本机执行输出
  git add 之后,暂存区里的 blob 内容:
  $ git cat-file -p :app.conf
  host=db.example.com
  password=<REDACTED>
  port=5432
  工作区里的文件内容(没变):
  $ cat app.conf
  host=db.example.com
  password=hunter2
  port=5432
  过滤器被调用: clean app.conf

filter 属性把文件交给两条命令:clean 在 add(和比较)时把工作区内容转成要存进 blob 的内容,smudge 在 checkout 时把 blob 转成工作区内容。blob 里存的是 clean 的输出。

触发时机

实测 · 本机执行输出
  git status(stat 没变,已过 racy 窗口): (没有调用过滤器)
  改了文件后 git status: clean app.conf
  git diff --stat: clean app.conf
  rm + git checkout -- app.conf: smudge app.conf
  检出之后工作区的内容:
  $ cat app.conf
  host=db.example.com
  password=<REDACTED>
  port=5432
  (clean 把密码换成 <REDACTED>,smudge 只是原样输出,所以检出后真实密码没了——clean 是有损的,工作区里改动过的版本只在你手里)

stat 没变的 git status 不调用过滤器;内容变了的 status、git diff 会调用 clean;checkout 调用 smudge。

有损的 clean

实测 · 本机执行输出
  $ git hash-object app.conf; git hash-object --no-filters app.conf; echo "(第一行带过滤器,第二行不带,两者不同)"
  71ed8d46a93931349875316b0dc188e70eba107b
  932399015780455b072248fbc431adae8e5cc092
  (第一行带过滤器,第二行不带,两者不同)

clean 有损(例如脱敏)时,smudge 无法还原,工作区被检出覆盖后真实内容就丢了;git hash-object --no-filters 可以看不经过滤的 hash,与带过滤器的 hash 不同。

互逆

实测 · 本机执行输出
  $ git cat-file -p :a.txt; cat a.txt; git status --short
  HELLO WORLD
  Hello World
  $ cat a.txt; git status --short; echo "(检出的是 hello world;重新 clean 得到 HELLO WORLD,与暂存区一致,所以 status 干净——clean(smudge(x)) == x 才是要求)"
  hello world
  (检出的是 hello world;重新 clean 得到 HELLO WORLD,与暂存区一致,所以 status 干净——clean(smudge(x)) == x 才是要求)

好的过滤器满足 clean(smudge(x)) == x:检出得到的内容再 clean 后要与暂存区一致,git status 才是干净的。检出的内容本身可以和原来不同(大小写被改变)。

merge driver

实测 · 本机执行输出
  $ git merge theirs 2>&1 | head -3; cat VERSION
  Auto-merging VERSION
  Merge made by the 'ort' strategy.
   VERSION | 2 +-
  version=5
  driver 的实际调用:
    driver 被调用: base=version=1 ours=version=3 theirs=version=5 标记长度=7 路径=VERSION
  (内置的文本合并会在这里报冲突;自定义 driver 退出 0 表示它已经把合并结果写进了 %A,git 采用)

merge=名字 加 merge.名字.driver 命令:git 把基础、ours、theirs 写成三个临时文件(%O %A %B),还传冲突标记长度 %L 和路径 %P;driver 把结果写回 %A,退出 0 表示成功,非 0 表示冲突。

内置属性

实测 · 本机执行输出
  merge=(默认 text)       → Auto-merging f.txt | CONFLICT (content): Merge conflict in f.txt | Automatic merge failed; fix conflicts and t;f.txt 内容: '共同的一行⏎<<<<<<< HEAD⏎ours 加的⏎=======⏎theirs 加的⏎>>>>>>> theirs'
  merge=binary          → warning: Cannot merge binary files: f.txt (HEAD vs. theirs) | Auto-merging f.txt | CONFLICT (content): Merge c;f.txt 内容: '共同的一行⏎ours 加的'
  merge=union           → Auto-merging f.txt | Merge made by the 'ort' strategy. |  f.txt | 1 +;f.txt 内容: '共同的一行⏎ours 加的⏎theirs 加的'

默认文本合并冲突时写标记;merge=binary 不合并,留 ours 并标记冲突;merge=union 把两边的新增行都保留,不冲突。

分步演示

clean/smudge 与 merge driver
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。过滤器与 driver 都是实验目录里的小 shell 脚本(PATH 前面加了该目录);“clean 触发”的次数来自脚本写的日志
    • 没有验证:长期运行的 filter.<名>.process 协议(git-lfs 用的);filter.<名>.required;ident 属性;textconv 与 diff 驱动;merge driver 在多个冲突块或二进制文件上的行为;rebase 与 cherry-pick 时 driver 的调用;大文件下过滤器的性能
    京ICP备2026067502号-1京公网安备11010502063487号