以观察、判断和行动三个面板呈现一次聚焦提交的审阅过程。

小提交应当讲清一个完整变化

有用的提交应当回答仓库行为发生了什么变化。它包含实现、能够说明该行为的验证,以及使用变化所需的文档。这里的“小”指修改理由有明确边界,而不是行数越少越好。例如,修正免运费门槛,需要同时修改计算函数和对应的回归断言;旁边正在编写的无关笔记应当继续留在工作区。把这些边界说明白,之后的读者才知道哪些变化必须一起存在。 暂存区决定这次留下的检查点 Git 区分工作区、暂存区和已经提交的文件树。编辑文件只会改变工作区;执行添加操作,才会把当时的内容放进暂存区。此后继续编辑,不会自动更新已经暂存的版本。因此,在当前目录运行测试成功,并不能证明即将提交的快照也能通过:测试可能依赖尚未暂存的修复、导入或配置。审阅时应看暂存差异,包括新增和删除;部分暂存造成实质差异时,应验证选中的那个快照。 同一个目录里有两件独立工作时,暂存区可以帮助保留边界。只选择当前目的需要的文件或代码块,同时检查它依赖的测试数据、导入、配置和说明是否齐全。只有几行实现,却遗漏必需的数据库结构变更,并不是可用的独立检查点。反过来,一次机械重命名即使涉及很多文件,只要变化理由一致,仍然可以是一个完整事项。文件数量不能替代对依赖关系的判断。 下面的小实验只有一组自包含文件,因此只复制改动文件就足够;真实仓库的暂存快照还必须包含未改动的已跟踪依赖、可执行模式以及必需的子模块。若不完整导出又意外从工作区导入依赖,就破坏了检查原本想建立的隔离,测试通过仍不能证明实际提交可用。 坏掉的样子 混合提交把本来无关的决策绑在一起。回退界面变化时,可能顺带移除藏在同一提交里的可靠性修复。随意按文件拆开也未必更好:如果第一个提交已经调用新接口,第二个提交才定义接口,中间状态就不能运行。应该保留有意义的中间版本;如果一组修改必须按顺序共同审阅,就明确写出依赖,而不是用很多碎片制造可独立交付的假象。 小提交能够减少审阅者需要同时理解的信息,却不保证反向操作没有冲突,更不保证生产回退安全。后续代码可能已经依赖之前的行为,数据库迁移也可能改变 Git 无法恢复的数据。revert 会记录一个新的反向变化,与中间发生的修改仍可能冲突。即使文本合并成功,也要重新检查最终行为及部署条件,不能把命令成功当作业务状态已经恢复。 二分定位同样需要历史版本上可靠的好坏判定。某个中间提交无法编译,不等于目标缺陷就是在这里引入的。跳过无法测试的版本,有时只能得到多个可能候选。保持检查点可运行有助于定位,但提交够小、标题够清楚,都不能修好一个会随机失败的测试,也不能补齐历史环境缺失的依赖。 快速验证 把下面代码保存为 small-commits.py,安装 Git 后执行 python3 small-commits.py。脚本创建临时仓库,隔离全局 Git 配置并使用空 hooks 目录,只在该仓库创建两个本地提交。实验约定:小计低于五十时收取五个单位运费,达到五十就免运费。初始函数错误地把恰好五十也算进了收费区间。 from pathlib import Path from tempfile import TemporaryDirectory import json import os import subprocess import sys with TemporaryDirectory() as directory: repo = Path(directory) / 'repo' repo.mkdir() hooks = Path(directory) / 'empty-hooks' hooks.mkdir() env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C', 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': os.devnull} def git(*args): return subprocess.check_output([ 'git', '-c', 'user.name=Example', '-c', 'user.email=example@example.invalid', '-c', 'commit.gpgsign=false', '-c', f'core.hooksPath={hooks}', '-c', 'core.autocrlf=false', *args], cwd=repo, env=env, text=True) git('init', '-q', '-b', 'main') (repo / 'price.py').write_text('def shipping(subtotal):\n return 5 if subtotal <= 50 else 0\n') (repo / 'test_price.py').write_text('from price import shipping\nassert shipping(49) == 5\n') (repo / 'notes.txt').write_text('unrelated draft\n') git('add', 'price.py', 'test_price.py', 'notes.txt') git('commit', '-qm', 'Add shipping calculation and its initial check') test = 'from price import shipping\nassert shipping(49) == 5\nassert shipping(50) == 0\n' (repo / 'test_price.py').write_text(test) failed = subprocess.run([sys.executable, '-B', 'test_price.py'], cwd=repo, capture_output=True, text=True) assert failed.returncode != 0 and 'AssertionError' in failed.stderr (repo / 'price.py').write_text('def shipping(subtotal):\n return 5 if subtotal < 50 else 0\n') (repo / 'notes.txt').write_text('unrelated draft, still being edited\n') git('add', 'price.py', 'test_price.py') staged = git('diff', '--cached', '--name-only').splitlines() assert staged == ['price.py', 'test_price.py'] snapshot = Path(directory) / 'index-snapshot' snapshot.mkdir() for name in staged: (snapshot / name).write_text(git('show', ':' + name)) subprocess.run([sys.executable, '-B', 'test_price.py'], cwd=snapshot, check=True) git('commit', '-qm', 'Make the free-shipping threshold inclusive') assert git('status', '--porcelain') == ' M notes.txt\n' assert git('show', 'HEAD:notes.txt') == 'unrelated draft\n' print(json.dumps({'regression_failed_before_fix': True, 'staged_files': staged, 'staged_test_passed': True, 'unrelated_edit_preserved': True})) 新增的边界断言首先会在旧函数上失败。修正之后,脚本仅暂存 price.py 和 test_price.py,再把这两个文件的暂存字节复制到另一目录,在那里执行断言,成功后才提交。输出同时记录修复前的失败和暂存快照的成功,因而能区分“测试存在”与“测试确实运行并通过”这两件事。 ...

七月 25, 2027 · 2 分钟 · XB Field Notes
文章标题旁是三个相连面板,英文标签依次为 Observe、Decide 和 Act。

git bisect 用自动探针定位回归

自动二分会反复检查历史版本,并把测试结果转成对提交的判断。在简单区间里,它通常能用近似对数级的探针次数替代大量手动检出,但前提是测试可信。包装脚本若把工具缺失或基础设施故障错误归类为产品回归,就可能给出看似明确、实际错误的提交。因此,设计判断契约本身就是调试工作的一部分,不能当作搜索外面无关紧要的准备步骤。 先确定能够复现的问题 选择一个可观察的回归,并用同一个执行器验证计划采用的正常端点和异常端点。某个发布名字被认为没问题,证据弱于在那个准确版本上实际检查成功。输入、工具链、依赖和相关环境应保持足够一致,让源码版本成为真正接受检验的变量;否则即使最后缩小到一个提交,也无法知道改变结果的究竟是代码还是环境。 启动时先给出异常版本,再给出已知正常版本。Git 会选择中间提交,并从当前检出的工作树调用探针。执行器应放在不会随历史切换而消失的稳定路径,或确保每个待测版本都包含可用实现。相对路径脚本在搜索一半时不存在,属于测试设施问题,不是产品恰好从那次提交开始损坏的证据。 按真实退出码契约映射结果 自动运行模式中,零表示正常;一到一百二十七表示异常,但一百二十五是例外,代表当前版本无法测试,需要跳过。其他状态值会中止自动搜索。这与“所有非零都说明发生回归”的简化说法不同,包装脚本必须明确区分结果,而不能把任何失败都交给 Git 当作同一种判断。 文档规定的异常区间包含常用于命令不可执行或不存在的一百二十六和一百二十七。应提前检查必需工具,并把识别出的基础设施失败映射到中止状态,例如二百。不能不看测试工具自身的含义,就原样转交所有非零返回。另外,超出可表示范围的 Shell 退出值可能被截断,因此需要选择明确、受支持的状态,不能随意用一个巨大数字表示致命错误。 跳过适用于某个确实无法测试、结论未知的版本,而不是所有构建失败。如果正在寻找的回归本来就是构建损坏,把构建失败跳过就等于丢掉主要证据。反过来,与症状无关的过时依赖,也不应自动把旧提交判坏。执行器为什么把两类情况区别对待,需要有可检查的依据,并在日志中保留相应原因。 坏掉的样子 测试输出经过管道时,最后的日志命令成功可能掩盖测试失败;清理命令也可能覆盖原状态。应先保存测试结果,再记录诊断,最后明确返回映射后的判断值。标准错误上出现了一段异常文字,并不会阻止 Git 接受意外返回的零。自动化读取的是状态契约,不会根据人类看起来很严重的日志重新推断好坏。 修改源码的测试、复用陈旧构建产物的测试,以及依赖持续变化远端服务的测试,都可能让不同版本之间失去可比性。应隔离生成文件,并保留足够日志解释每次判断。不要为了让探针可重复,就清空宽泛的用户目录;应使用可丢弃且归属明确的测试环境,把清理范围限定到自己创建的路径。 靠近转换边界的跳过,可能留下多个首坏候选。随机失败或非单调变化的判断条件,也会破坏简单边界假设;增加次数不能修复错误标签。归因之前应检查搜索日志,在报告出的边界及相关父级重新测试。提交图、跳过版本和不同构建耗时还意味着,对数级探针次数并不等于固定的实际运行时间。 快速验证 使用 Python 3 和 Git 2.28 或更新版本运行下面实验。临时仓库排除个人 Git 配置和路由变量,关闭钩子与签名,没有远端。九个合成版本编码正常、异常或不可测状态;第七号版本首次变坏,第四号版本专门触发跳过路径。执行器位于检出仓库之外,因此切换提交不会把它移走或替换成旧版本。 import os import subprocess import sys import tempfile from pathlib import Path env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull) with tempfile.TemporaryDirectory(prefix="git-bisect-run-") as directory: root = Path(directory) repo, probe = root / "repo", root / "probe.py" repo.mkdir() probe.write_text('''from pathlib import Path import sys try: state = Path("state.txt").read_text().strip() except OSError: sys.exit(200) code = {"good": 0, "bad": 1, "skip": 125}.get(state, 200) print(f"probe={state} code={code}") sys.exit(code) ''') def git(*args, check=True): return subprocess.run( ["git", "-c", "user.name=Bisect Example", "-c", "user.email=bisect@example.invalid", "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", *args], cwd=repo, env=env, text=True, capture_output=True, check=check, ) git("init", "-q", "-b", "main") commits = [] for number in range(9): state = "skip" if number == 4 else ("bad" if number >= 7 else "good") (repo / "state.txt").write_text(state + "\n") git("add", "state.txt") git("commit", "--allow-empty", "-qm", f"revision {number}") commits.append(git("rev-parse", "HEAD").stdout.strip()) codes = [] for revision in (commits[0], "main"): git("switch", "-q", "--detach", revision) codes.append(subprocess.run([sys.executable, str(probe)], cwd=repo, text=True, capture_output=True).returncode) git("switch", "-q", "main") assert codes == [0, 1], codes print("endpoints-verified:", True) git("bisect", "start", commits[-1], commits[0]) try: run = git("bisect", "run", sys.executable, str(probe)) assert "probe=skip code=125" in run.stdout, run.stdout print("skip-observed:", True) assert git("rev-parse", "refs/bisect/bad").stdout.strip() == commits[7] print("first-bad-correct:", True) finally: git("bisect", "reset") git("bisect", "start", commits[-1], commits[0]) try: before = git("bisect", "log").stdout fatal = git("bisect", "run", sys.executable, "-c", "raise SystemExit(200)", check=False) assert fatal.returncode != 0 and git("bisect", "log").stdout == before print("fatal-did-not-classify:", True) finally: git("bisect", "reset") assert git("symbolic-ref", "--short", "HEAD").stdout.strip() == "main" assert git("rev-parse", "HEAD").stdout.strip() == commits[-1] assert not git("status", "--porcelain").stdout print("original-checkout-restored:", True) 五个观察会先由断言检查,再打印真值;样本若不满足契约会以失败状态退出,不会只是显示一个假值然后仍然成功返回。执行器先确认两个端点,搜索中实际遇到跳过码,仍然正确定位第七号版本。第二次搜索故意返回二百,应中止且不向二分日志增加新的好坏判断。两次搜索最后都恢复原分支与原末端,并保持工作树干净,避免把仍处于排查状态误当作已经回到正常开发位置。 ...

七月 7, 2027 · 2 分钟 · XB Field Notes
文章标题旁边是标有 Observe、Decide、Act 的三个面板。

git tag 把一次发布固到提交

发布标签为某个源码版本提供一个容易交流的名字。它与分支不同,创建新提交时不会自动前进。但 Git 并没有从物理上禁止标签变化:拥有相应权限的人仍能替换或删除标签引用。可靠的发布约定来自几件事共同作用:记录对象标识、坚持发布名称不可变,以及限制谁能够更新发布引用。不能仅凭“用了标签”就断言任何人拿到的代码一定相同。 两种标签 轻量标签是直接指向某个对象的引用,通常指向提交。附注标签则指向单独的标签对象,其中保存目标、打标签者身份、时间、说明,以及可选的签名。打标签的人并不一定是提交作者;有了这些元数据,也不意味着附注已经经过密码学签名。发布记录需要说明谁作出发布决定,而不是把提交作者与发布者混为一谈。 对于附注标签,git rev-parse refs/tags/v2.4.1 得到标签对象标识,git rev-parse 'refs/tags/v2.4.1^{commit}' 则解引用到目标提交。当说明或签名身份也是发布证据的一部分时,应同时记录二者。只比较提交会漏掉标签留言或签名者变化;只比较可读名称,则连源码是否一致也无法确定。对象标识与名称各自承担不同的核对职责。 为什么给发布签名 git tag -v 使用配置的签名后端及本地信任材料验证签名标签。密码学验证成功之后,还需要确认该密钥确实属于项目发布策略认可的人。一个陌生人的有效签名,并不能证明维护者批准了这次发布。密钥的来源、授权范围和撤销状态,都应由实际发布流程明确管理,而不是靠命令里出现“成功”两个字代替。 源码标签有签名,也不能直接证明下载的二进制就是从该源码构建的。制品校验和、构建来源记录及构建过程,需要补上这条独立关系。另外,不要把验签命令接到 head 后面,再依赖管道最后一项的退出码:只截取几行输出可能掩盖验签失败。发布检查应保留并判断验签命令自身的结果,显示多少行只是界面选择。 移动要格外小心 修正尚未分享的本地标签,与替换消费者已经获取的标签,是两种不同操作。标签一旦分发,应优先使用新版本名称,并记录旧版本被撤回的原因。修补安全缺陷也不要求重写旧标签;保留原引用反而有助于识别受影响部署。服务端的引用保护应阻止未经授权的发布标签更新与删除,不能只靠客户端的习惯维持这个约定。 如果特殊情况下确实批准替换,应公布准确的新旧对象标识,并只协调那个明确的引用。把所有标签一起强制获取,会覆盖其他无关的本地引用,不应成为常规修复建议。消费者需要知道自己持有的是哪一个对象,不能让两个不同提交在没有说明的情况下继续共享同一个发布名称。 坏掉的样子 latest 这类浮动名称可能在两次构建之间解析到不同提交。即便界面保留方便阅读的名字,也应为每个制品和每次部署记录真正解析到的提交标识。同名不保证两次构建一致,而固定提交也不会自动固定外部依赖或编译器版本。源码定位与可复现构建有关,却不能互相替代。 另一种故障是发布标签只存在于某个本地克隆。普通分支推送不一定传输它;--follow-tags 处理的是从推送提交可达、符合条件的附注标签,不是本地全部标签。应使用 git ls-remote --tags 核对准确的远端引用及对象标识,不能对版本号作模糊子串匹配,否则可能把另一个版本当成目标。远端可见只能证明分发,不能证明构建正确或签名可信。 快速验证 在 POSIX 系统上用 Python 3 及 Git 运行下面的实验。它创建独立临时仓库,使用示例身份,为演示明确关闭签名,并且不配置远端。所有提交和标签都位于这个临时仓库,不会给当前项目打标签;退出后由临时目录管理器清理。命令通过参数列表调用,不需要拼接或解释来自外部的 Shell 文本。 ...

六月 11, 2027 · 1 分钟 · XB Field Notes
文章标题旁边是标有 Observe、Decide、Act 的三个面板。

git 分支是一个会移动的标签

Git 分支为一条历史线的末端提供名字。新建分支通常只是增加一个指向既有提交的引用,不会再复制一份全部受跟踪文件。两个名字可以先指向同一提交,再分别前进。这让分支创建成本很低,但不意味着名字内部装着历史,更不意味着删除指向某个提交的全部引用后,内容仍会永远保留。理解分支时,需要把名称、对象和保留条件分开。 它是引用,不是固定的存储文件 本地分支属于 refs/heads/ 引用命名空间,具体表示方式是实现细节:引用可能独立存储、被打包,或使用其他受支持的后端。不要假定一定存在某个松散文件,就直接编辑 Git 内部目录来移动分支。应使用 Git 的引用命令,让它遵守仓库实际的存储结构和更新规则,而不是把一种磁盘布局当作永久接口。 提交记录目录树和父提交,沿着父级关系向前追溯,就得到分支末端可达的历史。因此,两条分支可以共享大部分祖先,却拥有不同的末端。分支名称不是提交身份的一部分,新增或重命名分支也不会改写它指向的提交。图中的对象关系解释了共享历史,名称只是人们访问这些对象的入口。 创建分支与切换分支也不同。git branch topic 在当前提交创建名称,但当前分支不会改变;git switch topic 才选中它,并按需要更新工作树。git switch -c topic 把创建与切换合在一起。记录新工作之前,应检查真正活动的分支,不能因为名称已经存在,就误以为接下来的提交一定会记在那条线上。 HEAD 决定哪个名字前进 第一次提交之前,HEAD 可以指向尚未诞生的分支名,此时分支引用还无法解析为提交。这与分离状态不同:名字已被选中,只是没有首个提交对象。仓库已有提交之后,通常的附着状态下,HEAD 通过符号引用指向当前分支。创建提交时,该分支前进到新对象,其他原本共享旧末端的分支保持不动;新提交的父级关系保留祖先。分支也可能因合并或其他明确更新引用的操作而移动,并不是只有普通提交才能改变它。因此,判断某个名字为什么变化,需要检查实际发生的操作,而不只看提交按钮是否被点击。 直接选中一个提交,可以进入分离状态。之后的新提交会推动 HEAD,但不会推动某条本地分支。提交本身是真实对象,只是切换离开后可能没有持久的分支名称指向它。离开之前可以在目标提交创建分支,或者之后凭已知对象标识重新命名。引用日志有助于恢复,却受到保留时间和垃圾回收影响,不能被当作永久保存承诺。 origin/main 这类远端跟踪引用,是本地对远端状态的观察,通常随获取操作更新;它不是实时连接,也不等同于本地 main。上游配置描述供相关命令使用的分支关系,不会在任意一方提交时自动同步两个末端。排查本地与远端差异时,应明确比较的是哪两个引用,以及远端状态上次何时被获取。 坏掉的样子 未提交改动属于工作树和索引,不是一份已经保存在分支里的快照。切换时,兼容的改动可能被一起带走;如果会覆盖当前修改,Git 也可能拒绝切换。应先检查工作区状态,再有意识地保存未完成工作。仅仅创建一个分支名字,并没有保存那些从未提交的编辑,不能据此清理或丢弃文件。 删除分支是删除引用,不是要求立即擦除它曾经到达的全部对象;其他引用可能继续保留那些提交。但如果最终没有任何保留路径,对象以后仍可能被清理。普通删除命令检查目标是否已经合入配置的上游;没有上游时,则检查是否合入当前 HEAD。这种保护检查的是图上关系,不是合并后程序是否正确,更不是对所有业务改动都已经保留的验收。 合并也不只是多加一个名字。快进会移动已有引用,而分叉历史的普通合并通常创建带结果目录树和多个父级的新提交。父级可达关系保留历史对象,却不保证每项贡献都正确地出现在合并后的文件中。审阅时既要看图,也要看内容,不能把“历史还能找到”解释成“最终行为没有丢失”。 快速验证 在 POSIX 系统上使用 Python 3 和 Git 2.28 或更新版本运行下面的实验。它只在私有临时仓库创建空提交,排除个人 Git 配置和继承的 Git 路由变量,关闭钩子及签名,不配置任何远端。删除尝试也只针对这个可丢弃仓库里的演示分支,不会影响当前项目的引用。实验结束后,临时目录管理器会清理整套演示数据。 ...

二月 5, 2027 · 2 分钟 · XB Field Notes
两行标题旁边的三个相连方块,依次标注 Observe、Decide 与 Act。

凭据应留在仓库历史之外

凭据一旦提交到 Git,就成为版本化对象关系的一部分,协作者和自动化系统都有可能复制它。从最新目录树删除文件,并不会删除先前包含它的提交。实际应建立的边界,是让凭据值不进入源码历史,再通过获得授权的运行时机制供应。仓库配置可以说明怎样取得秘密,而不需要把秘密本身写进可复制的源文件。 坏掉的样子 一个看起来干净的检出,可能掩盖历史版本中已经暴露的值。新克隆通常会收到可达历史,包括已删除文件的旧版本。不能说 Git 在任何清理策略下都会永远保留所有对象,但“删掉今天的文件”确实不等于清理历史。即使重写一个仓库,也无法召回已经独立分发出去的克隆、分叉、缓存和构建产物,清理某一位置不能代表所有副本都已消失。 面对真实泄漏,应先撤销或替换相关凭据,并确认哪些系统会接受它。删除一行改变的是发布内容,不是认证状态。历史重写应单独协调,因为它会改变提交标识,也可能打断协作者的工作。说明事件时,不能再把原值复制到问题单、提交说明、扫描报告或事故笔记里,否则处理过程本身又制造了新的传播位置。 运行时注入也不会自动保密。环境值可能被子进程继承或被诊断工具采集;文件可能被具有足够访问权限的身份读取。应限制消费服务的权限,控制交付通道,避免把真实值写入日志和生成结果。即使文件位于仓库外,也仍然需要明确的拥有者与生命周期,不能把“没有提交到 Git”当作全部秘密管理工作已经完成。 快速验证 在安装了 Git 的 POSIX 系统上运行下列 Python 脚本。它使用明确的虚构标记,绝不使用任何有效凭据;两个仓库和运行时文件都位于同一个临时目录,退出后一起清理。夹具排除继承的 GIT_* 覆盖值、系统及全局配置和模板钩子,记录中的 Git 版本为 2.50.1: from pathlib import Path from tempfile import TemporaryDirectory import io,os,stat,subprocess,sys,tarfile env={key:value for key,value in os.environ.items() if not key.startswith('GIT_')} env.update(GIT_CONFIG_NOSYSTEM='1',GIT_CONFIG_GLOBAL=os.devnull) with TemporaryDirectory(prefix='credential-repo-lab-') as directory: base=Path(directory);repo=base/'source';repo.mkdir() def git(*args,cwd=repo,binary=False): result=subprocess.run(['git',*args],cwd=cwd,env=env,capture_output=True,text=not binary) assert result.returncode==0,result.stderr return result.stdout git('init','-q','--template=');git('config','user.name','Credential Lab') git('config','user.email','credentials@example.invalid') git('config','commit.gpgsign','false') dummy='EXAMPLE-ONLY-NOT-A-REAL-SECRET\n' (repo/'fixture.env').write_text(dummy) (repo/'app.txt').write_text('read the configured credential file at runtime\n') git('add','--','fixture.env','app.txt');git('commit','-qm','Synthetic exposure fixture') original=git('rev-parse','HEAD').strip() git('rm','--','fixture.env');git('commit','-qm','Remove fixture from current tree') assert not (repo/'fixture.env').exists() assert git('show',original+':fixture.env')==dummy clone=base/'clone';git('clone','-q','--no-local',str(repo),str(clone)) assert not (clone/'fixture.env').exists() assert git('show',original+':fixture.env',cwd=clone)==dummy print('current file absent; historical fixture readable in source and clone') archive=git('archive','--format=tar','HEAD',binary=True) with tarfile.open(fileobj=io.BytesIO(archive),mode='r:') as bundle: assert bundle.getnames()==['app.txt'] print('current-tree archive excludes fixture; Git history still contains it') runtime=base/'runtime';runtime.mkdir(mode=0o700) secret_file=runtime/'credential';secret_file.touch(mode=0o600) secret_file.write_text(dummy) assert stat.S_IMODE(secret_file.stat().st_mode)==0o600 child_env=dict(env,CREDENTIAL_FILE=str(secret_file)) child="from pathlib import Path; import os; p=Path(os.environ['CREDENTIAL_FILE']); assert p.read_text(); print('credential loaded without printing its value')" result=subprocess.run([sys.executable,'-c',child],cwd=clone,env=child_env,text=True,capture_output=True) assert result.returncode==0,result.stderr assert dummy.strip() not in result.stdout+result.stderr print(result.stdout.strip()) 第一项确认,两个当前工作区都没有样本文件,但都能通过早先的提交读取它的原始内容。克隆使用普通 Git 传输路径,关闭本地硬链接优化,因此验证的是另一个实际建立起来的对象数据库,不只是两个路径恰巧指向同一份文件。脚本在内部比较虚构标记,结果只说明是否保留,没有打印它的值,保留了检查与泄漏输出之间的区别。 ...

九月 14, 2026 · 1 分钟 · XB Field Notes