git-secrets 版本演进全解析:从 1.0.0 到 1.3.0 的扫描机制、钩子行为与兼容性修复
git-secrets 版本演进全解析:从 1.0.0 到 1.3.0 的扫描机制、钩子行为与兼容性修复
本文以仓库根目录下的 CHANGELOG.md 为脉络,逐版本解读 git-secrets 自 2015 年 12 月发布 1.0.0 以来,直至 1.3.0(2019-02-10)的每一次关键变更。git-secrets 是一个用于防止把密码、API 密钥等敏感信息提交进 git 仓库的 Bash 脚本工具;读懂它的版本历史,等于理解了它"如何扫描、在什么时机扫描、如何过滤误报"的完整设计。读完本文,你将掌握 --scan / --scan-history 的底层调用链、pre-commit 钩子为何必须扫描 staged 文件、.gitallowed 白名单机制、secret provider 的空输出处理,以及围绕 IFS、git exec-path、Bash 4 等环境兼容性问题的具体修复方案,并且每一条结论都能在仓库源码与 Bats 测试中找到对应依据。
总览:CHANGELOG 与代码的对应关系
CHANGELOG.md 记录了 6 个正式版本,仓库的主实现集中在根目录的单文件脚本 git-secrets(共 360 行 Bash),测试则位于 test 目录下的四组 Bats 用例(git-secrets.bats、pre-commit.bats、commit-msg.bats、prepare-commit-msg.bats)。下表先给出全局映射,后文逐版本展开:
| 版本 | 日期 | 核心主题 | 对应源码位置 |
|---|---|---|---|
| 1.0.0 | 2015-12-10 | 首次发布 | git-secrets |
| 1.0.1 | 2016-01-11 | 含空格文件名、超大仓库扫描 | regular_grep、git_grep |
| 1.1.0 | 2016-04-06 | 钩子扫描 staged 文件、--scan-history、.gitallowed、--cached/--no-index/--untracked |
pre_commit_hook、scan_history、load_allowed |
| 1.2.0 | 2016-05-23 | .gitallowed 忽略空行与注释、含空格文件名、数字与字符串比较修复 |
load_allowed |
| 1.2.1 | 2016-06-27 | IFS 导致的 provider "command not found" 修复 | load_patterns |
| 1.3.0 | 2019-02-10 | 空 provider 输出过滤、git exec-path 支持空格、含换行/回车模式、纯 \n 模式忽略、Bash 4 修复、IAM 扫描收窄 |
load_patterns、register_aws |
1.0.0:初始发布确立的三大扫描管道
1.0.0 是 git-secrets 的首次发布(2015-12-10),其设计核心在 README.rst 的 Synopsis 与 Description 中给出了权威定义:
git secrets --scan [-r|--recursive] [--cached] [--no-index] [--untracked] [<files>...]
git secrets --scan-history
git secrets --install [-f|--force] [<target-directory>]
git secrets --list [--global]
git secrets --add [-a|--allowed] [-l|--literal] [--global] <pattern>
git secrets --add-provider [--global] <command> [arguments...]
git secrets --register-aws [--global]
git secrets --aws-provider [<credentials-file>]
它通过安装三个 git 钩子来拦截敏感信息入库(对应 install_all_hooks 与 install_hook):
- pre-commit:检查本次提交变更的文件是否命中禁止模式;
- commit-msg:检查提交消息是否命中禁止模式(commit_msg_hook);
- prepare-commit-msg:在非快进(non-fast-forward)合并时检查待合入的历史中是否存在违规内容(prepare_commit_msg_hook)。
扫描使用的正则基于 egrep(扩展正则),主脚本通过 git grep -nwHEI 或 grep -nwHEI 执行(见 git_grep 与 regular_grep),其中 -w 表示按词边界匹配、-E 为扩展正则、-I 忽略二进制文件。README.rst 特别提醒:macOS 与 Linux(BSD vs GNU)上的 egrep 版本不同,正则行为可能有差异,这是后续兼容性修复的伏笔。
从 1.0.0 开始,--install 就把三个钩子写成了极简的转发脚本——每个钩子文件的内容只有一行:
#!/usr/bin/env bash
git secrets --<hook> -- "$@"
这也是为什么 git-secrets 必须放进 PATH(README.rst 的 Installing git-secrets 一节明确要求),钩子才能被 git 正确调用。
1.0.1:文件名含空格与十万级文件仓库的扫描修正
修复一:含空格文件名的正则扫描
1.0.1(2016-01-11)修复了"仓库中包含空格的文件名在 git secrets --scan(未提供文件名、经由 git grep)时无法正确处理"的问题。从源码看,git_grep 把用户传入的文件列表通过 "${files[@]}" 以数组方式传给 git grep,并在参数列表前使用 -- 分隔(git_grep):
git_grep() {
local options="$1"; shift
local files=("${@}") combined_patterns=$(load_combined_patterns)
[ -z "${combined_patterns}" ] && return 1
GREP_OPTIONS= LC_ALL=C git grep -nwHEI ${options} "${combined_patterns}" -- "${files[@]}"
}
-- 之后的内容会被 git 视为路径而非选项,从而正确容纳带空格的路径。与之对应,pre-commit.bats 中专门有 "Rejects commits with prohibited patterns in changeset with filename that contain spaces" 用例,构造了 da ta.txt 这样的文件名并断言提交被拒绝且输出 da ta.txt:1:@todo more stuff。
修复二:数十万文件规模仓库的扫描
同版本还修复了"拥有数十万个文件的仓库在 git secrets --scan(未提供文件名)时"的可用性问题。关键在 scan() 的分流逻辑(scan):
scan() {
local files=("${@}") options=""
[ "${SCAN_CACHED}" == 1 ] && options+="--cached"
[ "${SCAN_UNTRACKED}" == 1 ] && options+=" --untracked"
[ "${SCAN_NO_INDEX}" == 1 ] && options+=" --no-index"
# Scan using git-grep if there are no files or if git options are applied.
if [ ${#files[@]} -eq 0 ] || [ ! -z "${options}" ]; then
output=$(git_grep $options "${files[@]}")
else
output=$(regular_grep "${files[@]}")
fi
process_output $? "${output}"
}
当没有显式指定文件时,扫描走 git grep 而非 grep,由 git 自行枚举 git ls-files 管理的文件清单(README.rst 的 --scan 说明:"If no files are provided, all files returned by git ls-files are scanned")。这意味着扫描规模从"逐个文件传给 grep"变为"git 索引内部的批量检索",在超大仓库中开销更低、也不受命令行参数长度限制。
1.1.0:功能分水岭——staged 扫描、全历史扫描与 .gitallowed
1.1.0(2016-04-06)是功能增长最集中的版本,CHANGELOG 记录了四项变更。
变更一:pre-commit 钩子从工作区改为扫描 staged 文件
CHANGELOG 原文指出:旧版 pre-commit 钩子只扫描工作目录(working directory)而非已暂存(staged)文件,一旦工作区与暂存区发生漂移,就会漏检或误报。1.1.0 将其改为扫描 staged 文件。
源码佐证在 pre_commit_hook:
pre_commit_hook() {
SCAN_CACHED=1
local files=() file found_match=0 rev="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
# Diff against HEAD if this is not the first commit in the repo.
git rev-parse --verify HEAD >/dev/null 2>&1 && rev="HEAD"
# Filter out deleted files using --diff-filter
while IFS= read -r file; do
[ -n "$file" ] && files+=("$file")
done <<< "$(git diff-index --diff-filter 'ACMU' --name-only --cached $rev --)"
scan_with_fn_or_die "scan" "${files[@]}"
}
要点有三:
- 通过
git diff-index --cached拿到暂存区相对 HEAD 的差异,--diff-filter 'ACMU'明确排除已删除(D)文件; - 首次提交时 HEAD 不存在,回退到空树对象
4b825dc642cb6eb9a060e54bf8d69288fbee4904(git 内置的 magic 空树 SHA),从而也能扫描首个提交; SCAN_CACHED=1会令scan()走git grep --cached,即只检索索引(index)中的 blob。
pre-commit.bats 的 "Scans staged files" 用例精确复现了漂移场景:把违规内容 @todo more stuff 写入 data.txt 并 git add,随后又在工作区把同一文件改成无违规的内容,再提交——提交仍必须失败,因为检测的是 staged 版本。这正是 CHANGELOG 所描述的"working directory 与 staging directory 漂移"回归测试。
变更二:新增 --scan-history 全历史扫描
--scan-history 用于扫描仓库的全部修订历史。实现见 scan_history:
scan_history() {
# git log does not support multiple patterns, so we need to combine them
local combined_patterns=$(load_combined_patterns)
[ -z "${combined_patterns}" ] && return 0
# Looks for differences matching the patterns, reduces the number of revisions to scan
local to_scan=$(git log --all -G"${combined_patterns}" --pretty=%H)
# Scan through revisions with findings to normalize output
output=$(GREP_OPTIONS= LC_ALL=C git grep -nwHEI "${combined_patterns}" $to_scan)
process_output $? "${output}"
}
工程上有两个值得注意的设计:
git log只接受单个模式,所以先通过 load_combined_patterns 把多个secrets.patterns用|拼接成一个正则;- 用
git log --all -G"<combined>" --pretty=%H先找出"diff 内容发生变化且匹配模式"的提交,仅对这些提交执行git grep,从而避免对整条历史全量暴搜——CHANGELOG 称之为"reduces the number of revisions to scan"。
git-secrets.bats 中有三组测试分别覆盖"历史含违规必须失败""仅工作区含违规但历史干净则 --scan-history 通过""违规分布在不同分支的历史中仍必须失败",验证 --all 确实覆盖了全部分支。
变更三:.gitallowed 白名单过滤误报
CHANGELOG 记载"Added the ability to filter false positives by using a .gitallowed file"。机制实现位于 load_allowed:
load_allowed() {
git config --get-all secrets.allowed
local gitallowed="$(git rev-parse --show-toplevel)/.gitallowed"
if [ -e "$gitallowed" ]; then
cat $gitallowed | awk 'NF && $1!~/^#/'
fi
}
.gitallowed 放置在仓库根目录,其行会被并入 allowed 正则集合。过滤发生在 process_output:
process_output() {
local status="$1" output="$2"
local allowed=$(load_allowed)
case "$status" in
0)
[ -z "${allowed}" ] && echo "${output}" >&2 && return 1
# Determine with a negative grep if the found matches are allowed
echo "${output}" | GREP_OPTIONS= LC_ALL=C grep -Ev "${allowed}" >&2 \
&& return 1 || return 0
;;
1) return 0 ;;
*) exit $status
esac
}
其判定语义(README.rst 的 Ignoring false positives 一节有完整描述)是:先把"命中禁止模式的整行文本(含 文件名:行号:内容 前缀)"全部取出,再对整组输出做反向匹配——只有所有命中行都被至少一条 allowed 正则覆盖时才算通过;只要有一行漏网就判为违规。因此 allowed 模式天然可以把文件名、行号写进正则做精确定位,例如:
# 白名单整个文件
git secrets --add --allowed '/tmp/example:.*'
# 白名单特定文件的特定行
git secrets --add --allowed '/tmp/example:3:.*'
注意:匹配对象是"文件名:行号:行内容"拼接后的完整字符串,这也是为什么 README 提醒"allowed 模式不要过度宽泛",避免把本应拦截的行一并放行。
变更四:--scan 支持 --cached / --no-index / --untracked
这三个选项扩展了 --scan 的检索范围,实现集中在 scan() 的选项拼装与 git_grep 的 git grep 透传:
| 选项 | 语义(README.rst) | 源码行为 |
|---|---|---|
--cached |
搜索索引文件(index)中已注册的 blob | git grep --cached |
--no-index |
搜索当前目录中未被 git 管理的文件 | git grep --no-index |
--untracked |
在跟踪文件之外,额外扫描未跟踪文件 | git grep --untracked |
同时,CHANGELOG 未提及但 README 明确规定了约束:-r|--recursive 与上述三个选项互斥。该约束在 git-secrets 的入口校验中强制执行,并在 git-secrets.bats 中有专门的互斥用例(--scan -r --cached 等组合必须退出码 1)。
1.2.0:.gitallowed 解析修正与跨平台数值比较
1.2.0(2016-05-23)包含三项修复。
修复一:.gitallowed 忽略空行与 # 注释
CHANGELOG 原文:"Now ignoring empty lines and comments (e.g., #) in the .gitallowed file." 源码即上节 load_allowed 中的 awk 'NF && $1!~/^#/':NF 过滤空行(NF 为 0 的行不输出),$1!~/^#/ 过滤以 # 开头的行。
对应的测试用例非常细致:
- git-secrets.bats "Empty lines must be ignored in .gitallowed files":空行不会让扫描意外通过;
- git-secrets.bats "Comment lines must be ignored in .gitallowed files":文件中写入
#hash不会放行#hash模式,而删除#写成hash才会放行——精确验证了注释语义。
修复二:含空格文件名的扫描(再次)
与 1.0.1 侧重 git grep 路径不同,1.2.0 修复的是"文件名含空格时 pre-commit 钩子扫描不正确"的问题。相关实现位于 regular_grep:
regular_grep() {
local files=("${@}") patterns=$(load_patterns) action='skip'
[ -z "${patterns}" ] && return 1
[ ${RECURSIVE} -eq 1 ] && action="recurse"
GREP_OPTIONS= LC_ALL=C grep -d "${action}" -nwHEI "${patterns}" "${files[@]}"
}
通过 Bash 数组 "${files[@]}" 把每个文件名作为独立参数传给 grep,从根源上避免了对含空格路径的字符串拆分。
修复三:数字与字符串比较导致的部分平台失败
CHANGELOG 原文:"Fixed an issue where numbers were being compared to strings causing failures on some platforms." 在 Bash 中,<a href="https://link.gitcode.com/i/cc0a647b5ffbe4b602dc3e2029ce591f" target="_blank">[ 1 == 1 ]] 与 [[ "1" == "1" ]] 语义不同,[[ 数字 == 字符串 ]] 在部分 shell/平台下会报错。主脚本中多处对开关量(如 FORCE、RECURSIVE、SCAN_CACHED)使用 [ ... -eq 1 ] 数值比较,例如 [install_hook 的 <a href="https://link.gitcode.com/i/3e1eea25ddfcb730d72308bf4daf54a7" target="_blank"> "${FORCE}" -ne 1 ]、[regular_grep 的 [ ${RECURSIVE} -eq 1 ]。1.2.0 统一了这类比较的写法,确保在数值与字符串处理差异较大的平台上行为一致。
1.2.1:IFS 污染导致的 provider "command not found"
1.2.1(2016-06-27)是一个小而关键的 Bug 修复,CHANGELOG 引用 PR #30 说明:secret provider 命令在执行时报 "command not found",根因是"previously set IFS variable"。
复现思路:Bash 中 IFS(Internal Field Separator)决定分词行为。若 IFS 被改成不含空格的取值(例如只含换行),那么形如 git secrets --aws-provider 的命令字符串在被展开执行时,git secrets 与 --aws-provider 的拆分就可能错乱,甚至把整条命令当成一个"命令"去查找,从而报 command not found。
修复落在 load_patterns 中执行 provider 的那一行:
load_patterns() {
git config --get-all secrets.patterns
# Execute each provider and use their output to build up patterns
git config --get-all secrets.providers | while read -r cmd; do
# Only split words on '\n\t ' and strip "\r" from the output to account
# for carriage returns being added on Windows systems. Note that this
# trimming is done before the test to ensure that the string is not empty.
local result="$(export IFS=$'\n\t '; $cmd | tr -d $'\r')"
# Do not add empty lines from providers as that would match everything.
if [ -n "${result}" ]; then
echo "${result}"
fi
done
}
export IFS=$'\n\t ' 在执行 provider 前显式把 IFS 重置为"换行、制表、空格"三字符集合,local result="$(export IFS=...; $cmd ...)" 的子 shell 作用域又保证了重置只在命令替换内部生效,不会污染外层脚本的 IFS。修复后的 IFS 语义同时为 1.3.0 的"provider 命令本身带参数"(如 cat /path/to/secret/file/patterns)奠定了正确分词基础。
1.3.0:空输出过滤、Windows 路径、换行模式与更精准的 IAM 扫描
1.3.0(2019-02-10)是 CHANGELOG 中最新的版本,修复项也最多。
修复一:空 provider 输出不再被纳入模式
CHANGELOG 原文:"Empty provider output is now excluded"(对应上游 issue #34)。原因在 load_patterns 的注释中写得很直白:空行会被 load_combined_patterns 拼进 | 连接的正则,而空模式会匹配一切内容,导致整个仓库被无条件判为违规。修复逻辑即:
if [ -n "${result}" ]; then
echo "${result}"
fi
provider 返回空串时直接跳过,不向模式集合输出任何行。git-secrets.bats 的 "Strips providers that return nothing" 用例注册了 echo(无输出)与 echo 123(有输出)两个 provider,断言只有 echo 123 被保留且扫描正常退出。
修复二:git exec-path 支持空格
CHANGELOG 原文:"Spaces are now supported in git exec path, making more Windows paths execute properly."
主脚本开头有这样一行(git-secrets):
. "$(git --exec-path)"/git-sh-setup
git 通过 git --exec-path 定位内置脚本目录(如 git-sh-setup),再以 . 命令 source 它。此前若 exec-path 直接拼接路径而未加引号,Windows 上形如 C:\Program Files\Git\mingw64\libexec\git-core 这类含空格的路径会因分词被拆开而 source 失败。现在用 "$(git --exec-path)"/git-sh-setup 双引号包裹命令替换结果,整个路径作为一个整体参与拼接,含空格的安装目录即可正常加载。
修复三:带换行与回车的模式正确加载,纯 \n 模式被忽略
CHANGELOG 两条关联变更:
- "Patterns with newlines and carriage returns are now loaded properly."
- "Patterns that contain only "\n" are now ignored."
前者与 Windows 有关:CRLF 环境下 provider 输出会带 \r 回车符,load_patterns 里 $cmd | tr -d $'\r' 统一剥掉回车,避免模式末尾粘上 \r 导致匹配失效;同时 provider 输出按行读取后进入模式集合并以 | 拼接,换行被正确处理为"多个模式"而非"一个含换行的畸形正则"。
后者是前者的边界条件:如果 provider 输出的只有 \n(即空行),tr 剥离后剩余为空串,配合 [ -n "${result}" ] 检查被直接丢弃——与修复一共享同一判定路径,防止空行污染 load_combined_patterns 的 | 拼接。
修复四:Various Bash 4 fixes
CHANGELOG 引用上游 issue #66 归纳了针对 Bash 4 的一系列修正。从代码现状可看到几类典型做法:
- 数组遍历统一使用
"${files<a href="https://link.gitcode.com/i/8d0fe5203c5078fef79fb4e3473eb1f9" target="_blank">@]}"引号展开([scan、git_grep、regular_grep),避免 Bash 4 下未加引号的数组展开行为差异导致文件名被分词; - 进程替换
<<< "$(git diff-index ...)"配合IFS= read -r file逐行读取(pre_commit_hook),确保以换行为唯一分隔、保留行内空格; - provider 执行时
export IFS=$'\n\t '显式重置分词集合(load_patterns),兼容 Bash 4 对 IFS 继承与子 shell 导出的细微差别。
这些写法保证脚本在 Bash 4.x(macOS 默认 Bash 即长期停留在 3.2,Linux 发行版普遍为 4.x+)环境下行为一致。
修复五:Make IAM key scanning much more targeted
CHANGELOG 原文:"Make IAM key scanning much more targeted." 对应 register_aws 中注册的 AWS 模式集合。1.3.0 之前仅注册通用的 Access Key ID 前缀模式,如今 --register-aws 写入的配置更为精细:
add_config 'secrets.patterns' '(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}'
add_config 'secrets.patterns' 'ABSK[A-Za-z0-9+/]{109,}=*' #Bedrock long-lived
add_config 'secrets.patterns' 'bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t' #Bedrock short-lived
add_config 'secrets.patterns' "${opt_quote}${aws}(SECRET|secret|Secret)?_?(ACCESS|access|Access)?_?(KEY|key|Key)${opt_quote}${connect}${opt_quote}[A-Za-z0-9/\+=]{40}${opt_quote}"
add_config 'secrets.patterns' "${opt_quote}${aws}(ACCOUNT|account|Account)_?(ID|id|Id)?${opt_quote}${connect}${opt_quote}[0-9]{4}\-?[0-9]{4}\-?[0-9]{4}${opt_quote}"
add_config 'secrets.allowed' 'AKIAIOSFODNN7EXAMPLE'
add_config 'secrets.allowed' "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
具体改进点:
- Access Key ID 的校验位规则更严格:前缀限定为
A3T/AKIA/AGPA/AIDA/AROA/AIPA/ANPA/ANVA/ASIA,后接恰好 16 位[A-Z0-9]; - 新增 Amazon Bedrock API key 模式:长期 key 形如
ABSK[A-Za-z0-9+/]{109,}=*,短期 key 为固定的bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t前缀; - Secret Access Key 与 Account ID 的赋值模式要求
:或=(允许可选的引号包裹),且分别限定 40 位 base64 字符与 4-4-4 位账户号格式,减少宽泛匹配; - 同时注册 AWS 官方示例 key(
AKIAIOSFODNN7EXAMPLE与wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY)为 allowed 模式,避免文档示例误报。
以上模式在 git-secrets.bats 的 "Adds common AWS patterns" 用例中被逐一断言存在。需要说明的是,README.rst 在 --register-aws 的说明中专门加了 note:这些模式"不保证捕获所有 AWS 凭据实例",git-secrets 只是额外保险,用户仍需自行尽职检查;这也适用于本文介绍的所有内置模式。
贯穿版本的三条主线:从 CHANGELOG 看设计原则
梳理 6 个版本,可以提炼出三条贯穿始终的工程主线,它们也是使用 git-secrets 时最重要的心智模型:
1. 扫描对象从"工作区"走向"git 索引与历史"
1.0.1 让无参 --scan 走 git grep(基于 git ls-files),1.1.0 让 pre-commit 只扫 staged 文件并新增 --scan-history 与 --cached,最终形成"提交时扫暂存、合并时扫历史、手动全历史体检"的完整覆盖。这也解释了为什么 pre-commit 钩子的输出格式统一为 文件名:行号:内容——它既是人类可读的错误信息,也是 process_output 做 allowed 反向匹配时的输入格式。
2. 模式集合的动态组装必须严防"空模式"
load_patterns → load_combined_patterns → git grep -E 的链路中,任何 provider 输出的空行都会变成 | 拼接出的空分支(匹配一切)。1.2.1 修复 IFS 保证 provider 命令能被正确拆分执行,1.3.0 又增加 [ -n "${result}" ] 空输出过滤与 tr -d $'\r' 回车剥离——两条修复共同守护这条链路的输入纯净性。
3. 兼容性修复集中在"引号、IFS、数组展开"三处 Bash 特性
含空格路径(1.0.1、1.2.0)、Windows 路径(1.3.0)、provider 命令找不到(1.2.1)、Bash 4 行为差异(1.3.0),本质都是 Bash 分词与展开规则在不同环境下表现不同。最终的代码形态统一为:数组参数用 "${files[@]}"、路径拼接用双引号包裹、IFS 在子 shell 中显式重置、逐行读取用 IFS= read -r。这些写法值得在同类 Bash 工具中直接复用。
验证与回归:如何用仓库自带的 Bats 测试复现修复
仓库使用 bats(Bash Automated Testing System)做回归测试,Makefile 的 test 目标直接驱动:
make test
# 等价于:LANG=C test/bats/bin/bats test/
测试覆盖与 CHANGELOG 的对应关系如下,读者可以据此逐条复现本文章结论:
| CHANGELOG 条目 | 对应测试 |
|---|---|
| 1.0.1 含空格文件名 | pre-commit.bats |
| 1.1.0 扫描 staged 文件 | pre-commit.bats |
1.1.0 --scan-history |
git-secrets.bats |
1.2.0 .gitallowed 忽略空行/注释 |
git-secrets.bats |
1.1.0 .gitallowed 过滤机制 |
git-secrets.bats |
| 1.3.0 空 provider 输出过滤 | git-secrets.bats |
| 1.3.0 收窄 IAM 模式 | git-secrets.bats |
| 1.1.0 互斥选项校验 | git-secrets.bats |
| 钩子安装(含 Debian 风格目录、模板目录) | git-secrets.bats |
四个钩子行为测试文件 pre-commit.bats、commit-msg.bats、prepare-commit-msg.bats 则分别验证三个钩子的拒绝/放行语义,其中 prepare-commit-msg.bats 的 "Rejects merges with prohibited patterns in history" 用例,正是对"--no-ff 合并必须拦截历史违规"这一 README 承诺的直接回归。
结语:版本历史即使用手册
git-secrets 的 CHANGELOG 虽短,却浓缩了它在三年多时间里处理真实问题的全部思路:扫描时机从工作区收束到暂存区、检索能力从单文件扩展到索引与全历史、模式管理从纯禁止模式扩展出 .gitallowed 与 secret provider 双层过滤、兼容性从 GNU/Linux 延伸到 macOS 与 Windows 的 Bash 差异。读懂这份 CHANGELOG,就基本掌握了 git-secrets 的能力边界与实现原理;配合 git-secrets 主脚本与 test 目录下的 Bats 用例,你既能回答"某个功能为什么这样设计",也能在遇到误报或漏报时准确定位到对应版本的修复逻辑。