Shell、命令、git、插件与执行支持工具
这一层像后台工具间,不是主流程本身,却支撑很多地方干活。它先认出该用 bash、PowerShell 还是 cmd,整理命令、环境变量和安全检查,再把命令交给普通进程、伪终端或沙箱去跑,并收好输出、退出码和子进程。Git 部分负责看仓库、分支、差异和安全调用 Git。插件部分负责打包、解包、安装和升级市场目录。还有外部编辑器、补丁定位等小工具,帮上层功能少踩坑。
Shell 建模和命令环境
这些文件定义如何检测和表示 shell、如何构建 shell 命令环境,以及向系统其他部分公开的共享命令解析辅助工具。
shell-command/src/lib.rs源码 ↗
项目里经常需要处理用户或系统要执行的命令,比如判断这是不是 Bash 命令、PowerShell 命令,或者这条命令会不会有危险。这个文件本身不写具体判断规则,而是像一个前台接待:把真正干活的模块摆出来,并决定哪些能力可以给外部使用。它公开了 shell_detect、bash、parse_command、powershell 这些模块,让别的代码可以解析和理解命令;同时把 command_safety 这个内部模块藏起来,只把 is_safe_command 和 is_dangerous_command 两个入口拿出来。这样外部代码不用知道里面复杂的安全规则,只要问“这条命令安全吗”或“这条命令危险吗”。如果没有这个文件,其他部分就得直接摸到各个内部模块,引用会更乱,也更容易用错。
shell-command/src/shell_detect.rs源码 ↗
这个文件像是在给程序找一位合适的“翻译员”。用户或系统给出的命令,不能直接凭空执行,通常要交给 shell(命令行外壳,也就是解释并运行命令的程序)来处理。这里先定义了支持的几种 shell 类型,再定义“检测到的 shell”这个结果,里面有类型和路径。查找时它会按顺序尝试:先信任调用方给的路径;再看当前用户系统默认 shell;再到系统环境变量 PATH 里找;最后检查一些常见安装位置。如果都找不到,就给一个保底选择:Windows 用 cmd.exe,其他系统用 /bin/sh。它还特别注意 Unix 系统上读取用户 shell 的方式,使用更安全的 getpwuid_r,避免多个线程同时读取系统用户信息时撞车。
ShellType::name16–24 ↗
DetectedShell::name34–36 ↗
fn name(&self) -> &'static str
作用:从一个已经检测好的 shell 结果里,拿到它的简短名字。这样外部代码不用关心里面的 shell_type 字段怎么翻译。
数据流:进去的是 DetectedShell 自己,里面有 shell 类型和路径;它只读取 shell_type,并交给 ShellType::name;出来的是类似 "zsh"、"bash"、"powershell" 的文字。
调用关系:它站在“检测结果”和“显示名称”之间。调用方拿到 DetectedShell 后想打印或转换时会用它,它自己把具体翻译工作交给 ShellType::name。
detect_shell_type39–59 ↗
fn detect_shell_type(shell_path: impl AsRef<std::path::Path>) -> Option<ShellType>
作用:根据一个路径或文件名猜出这是哪种 shell。比如 /bin/bash、bash、bash.exe 都应该被认成 Bash。
数据流:进去的是一个路径;它先看整个路径字符串是不是 zsh、bash、sh、cmd、pwsh 或 powershell;如果不是,就剥掉目录和扩展名,只看文件主名再试一次;出来的是识别出的 ShellType,识别不了就返回 None。
调用关系:它是整个文件里的“认名字”步骤。get_shell_path 用它确认用户默认 shell 是否符合想找的类型,get_shell_by_model_provided_path 用它判断外部给来的路径属于哪种 shell。
调用图:被 2 处调用(get_shell_by_model_provided_path, get_shell_path);外部调用 4 个(as_os_str, as_ref, file_stem, new)。
get_user_shell_path123–125 ↗
fn get_user_shell_path() -> Option<PathBuf>
作用:读取当前系统账号默认配置的 shell 路径。比如在 Unix/macOS/Linux 上,用户账号信息里可能写着默认 shell 是 /bin/zsh。
数据流:进去不需要业务参数;在 Unix 上它读取当前用户 ID,再向系统用户数据库询问这个用户的 shell 字段;成功时出来一个 PathBuf 路径,失败或非 Unix 平台就出来 None。它只读取系统信息,不修改系统。
调用关系:它给默认选择流程提供第一手线索。default_user_shell 会直接用它找当前用户喜欢的 shell,get_shell_path 也会用它判断系统默认 shell 是否刚好就是正在寻找的类型。
调用图:被 2 处调用(default_user_shell, get_shell_path);外部调用 9 个(from_ptr, uninit, from, getpwuid_r, getuid, sysconf, null_mut, try_from, vec!)。
file_exists127–133 ↗
fn file_exists(path: &std::path::Path) -> Option<PathBuf>
作用:检查某个路径是不是真有一个文件。这里不是只看名字像不像,而是确认磁盘上确实有这个可执行文件位置。
数据流:进去的是一个文件路径;它读取文件元数据,判断路径存在并且是普通文件;如果通过,出来同一个路径的 PathBuf,失败就出来 None。
调用关系:它是查找 shell 时的“验货员”。get_shell_path 在接受调用方提供的路径、系统默认路径、常见兜底路径之前,都会靠它确认文件真的存在。
调用图:被 1 处调用(get_shell_path);外部调用 2 个(from, metadata)。
get_shell_path135–164 ↗
fn get_shell_path(
shell_type: ShellType,
provided_path: Option<&PathBuf>,
binary_name: &str,
fallback_paths: &[&str],
) -> Option<PathBuf>
作用:按一套优先级找到某一种 shell 的实际路径。它解决的是“我想要 Bash,但 Bash 到底装在哪里”的问题。
数据流:进去的是想找的 shell 类型、可选的指定路径、二进制名字和一些备用路径;它先检查指定路径,再看用户默认 shell 是否匹配,再用 which 在 PATH 里找,最后试备用路径;出来是找到的 PathBuf,找不到就是 None。
调用关系:它是各类 shell 查找函数共用的主干。get_zsh_shell、get_bash_shell、get_sh_shell、get_powershell_shell、get_cmd_shell 都把自己的名字和备用路径交给它,由它完成实际寻找。
调用图:调用 3 个内部函数(detect_shell_type, file_exists, get_user_shell_path);被 5 处调用(get_bash_shell, get_cmd_shell, get_powershell_shell, get_sh_shell, get_zsh_shell);外部调用 2 个(new, which)。
get_zsh_shell168–175 ↗
fn get_zsh_shell(path: Option<&PathBuf>) -> Option<DetectedShell>
作用:专门寻找 zsh,并把找到的路径包装成统一的 DetectedShell 结果。zsh 是 macOS 和很多开发环境里常见的 shell。
数据流:进去的是一个可选的 zsh 路径;它调用通用的 get_shell_path,指定要找 Zsh、二进制名是 zsh、备用位置是 /bin/zsh;找到后出来 DetectedShell,找不到出来 None。
调用关系:它是 get_shell 分发到 Zsh 时走的分支。自己不重新实现查找规则,而是把具体查找交给 get_shell_path。
调用图:调用 1 个内部函数(get_shell_path);被 1 处调用(get_shell)。
get_bash_shell179–186 ↗
fn get_bash_shell(path: Option<&PathBuf>) -> Option<DetectedShell>
作用:专门寻找 bash,并返回统一格式的检测结果。bash 在 Linux 和许多脚本里非常常见,所以是重要候选项。
数据流:进去的是一个可选路径;它让 get_shell_path 按 Bash 的规则查找 bash,并检查 /bin/bash、/usr/bin/bash 等常见位置;出来是 DetectedShell 或 None。
调用关系:它由 get_shell 在需要 Bash 时调用。它只负责填好 Bash 的参数,真正找路径的活交给 get_shell_path。
调用图:调用 1 个内部函数(get_shell_path);被 1 处调用(get_shell)。
get_sh_shell190–197 ↗
fn get_sh_shell(path: Option<&PathBuf>) -> Option<DetectedShell>
作用:专门寻找 sh。sh 是 Unix 系统上最基础、最保守的 shell,经常被当作最后兜底。
数据流:进去的是一个可选路径;它调用 get_shell_path,指定类型是 Sh、名字是 sh、备用路径是 /bin/sh;成功出来 DetectedShell,失败出来 None。
调用关系:它是 get_shell 的 Sh 分支。default_user_shell_from_path 和 ultimate_fallback_shell 的整体策略里,sh 通常是非 Windows 系统的安全底线。
调用图:调用 1 个内部函数(get_shell_path);被 1 处调用(get_shell)。
get_powershell_shell215–230 ↗
fn get_powershell_shell(path: Option<&PathBuf>) -> Option<DetectedShell>
作用:专门寻找 PowerShell。它会优先找新版的 pwsh,找不到再试老名字 powershell。
数据流:进去的是一个可选路径;它先让 get_shell_path 找 pwsh,并带上平台相关的常见安装位置;如果失败,再找 powershell;出来是 PowerShell 类型的 DetectedShell 或 None。
调用关系:它由 get_shell 在需要 PowerShell 时调用。Windows 默认 shell 选择会优先依赖这个分支,因为 Windows 上 PowerShell 比 bash/zsh 更符合系统习惯。
调用图:调用 1 个内部函数(get_shell_path);被 1 处调用(get_shell)。
get_cmd_shell232–239 ↗
fn get_cmd_shell(path: Option<&PathBuf>) -> Option<DetectedShell>
作用:专门寻找 Windows 的 cmd。cmd 是 Windows 自带的传统命令解释器,也是 Windows 上最稳的保底选择之一。
数据流:进去的是一个可选路径;它调用 get_shell_path,指定类型是 Cmd、二进制名是 cmd,没有额外备用路径;成功出来 DetectedShell,失败出来 None。
调用关系:它由 get_shell 在需要 Cmd 时调用。ultimate_fallback_shell 在 Windows 上会直接给 cmd.exe,作为所有查找失败后的兜底。
调用图:调用 1 个内部函数(get_shell_path);被 1 处调用(get_shell)。
ultimate_fallback_shell241–253 ↗
fn ultimate_fallback_shell() -> DetectedShell
作用:给出最后的保底 shell,保证程序总能拿到一个结果。即使前面所有检测都失败,也不会让调用方完全没东西可用。
数据流:进去不需要参数;它判断当前是否是 Windows;Windows 出来 Cmd 加 cmd.exe,其他系统出来 Sh 加 /bin/sh。它不检查文件是否存在,只给一个最常见的默认路径。
调用关系:它是整个检测链条的安全网。get_shell_by_model_provided_path 和 default_user_shell_from_path 在找不到合适 shell 时会落到这里,避免流程中断。
get_shell_by_model_provided_path255–259 ↗
fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> DetectedShell
作用:处理外部模型或上层逻辑给来的 shell 路径。它不会盲信这个路径,而是先判断它像哪种 shell,再按本文件的规则确认能不能用。
数据流:进去的是一个路径;它先用 detect_shell_type 识别类型,再尝试按这个类型找到可用 shell;如果识别失败或找不到,就出来 ultimate_fallback_shell 的保底结果。
调用关系:它是“外部指定路径”进入本文件的入口之一。它先请 detect_shell_type 认名字,然后进入统一查找和兜底流程。
调用图:调用 1 个内部函数(detect_shell_type);被 1 处调用(get_shell_by_model_provided_path)。
get_shell261–269 ↗
fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<DetectedShell>
作用:按照指定的 shell 类型,分派到对应的查找函数。调用方只要说“我要 Bash”或“我要 PowerShell”,不用自己挑具体函数。
数据流:进去的是 ShellType 和一个可选路径;它根据类型选择 get_zsh_shell、get_bash_shell、get_powershell_shell、get_sh_shell 或 get_cmd_shell;出来是对应的 DetectedShell 或 None。
调用关系:它是本文件的总分发器。default_user_shell_from_path 会用它尝试用户默认 shell 和备选 shell,各个具体查找函数再把路径搜索交给 get_shell_path。
调用图:调用 5 个内部函数(get_bash_shell, get_cmd_shell, get_powershell_shell, get_sh_shell, get_zsh_shell);被 2 处调用(get_shell, default_user_shell_from_path)。
default_user_shell271–273 ↗
fn default_user_shell() -> DetectedShell
作用:找出当前用户默认应该用的 shell。外部代码想“按用户系统习惯运行命令”时,通常会从这里开始。
数据流:进去不需要参数;它先调用 get_user_shell_path 读取系统里当前用户的默认 shell 路径,再把这个路径交给 default_user_shell_from_path;出来一定是一个 DetectedShell,因为失败时也会有保底。
调用关系:它是默认 shell 检测的公开入口。它负责从系统拿原始路径,真正的选择和兜底策略交给 default_user_shell_from_path。
调用图:调用 2 个内部函数(default_user_shell_from_path, get_user_shell_path);被 2 处调用(default_user_shell, local)。
default_user_shell_from_path275–295 ↗
fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> DetectedShell
作用:根据一个“用户默认 shell 路径”推导最终要用的 shell。它也负责不同操作系统上的偏好顺序,比如 Windows 更偏向 PowerShell。
数据流:进去的是可选的用户默认 shell 路径;Windows 上它直接优先找 PowerShell;非 Windows 上先识别用户默认 shell,再按系统习惯补试 zsh 或 bash,最后失败就用 ultimate_fallback_shell;出来一定是 DetectedShell。
调用关系:它承接 default_user_shell 读到的系统路径,也方便测试或上层代码传入一个已知路径来走同样逻辑。它会通过 get_shell 调用各个具体 shell 查找分支。
调用图:调用 1 个内部函数(get_shell);被 2 处调用(default_user_shell_from_path, default_user_shell);外部调用 1 个(cfg!)。
tests::test_detect_shell_type303–367 ↗
fn test_detect_shell_type()
作用:验证 detect_shell_type 能正确识别常见 shell 名字和路径。它防止以后改代码时,把 bash.exe、/bin/zsh、pwsh 这类情况不小心弄坏。
数据流:进去没有运行时业务输入;测试里准备了一组路径样例,逐个调用 detect_shell_type 并用 assert_eq! 比较预期结果;出来是测试通过或失败,不改动真实系统配置。
调用关系:它只在测试时运行。它围绕 detect_shell_type 这个基础识别函数做保护,因为后面的默认选择和路径查找都依赖识别结果是否靠谱。
调用图:外部调用 1 个(assert_eq!)。
core/src/shell.rs源码 ↗
运行一条命令并不是简单把文字丢给系统就行。不同外壳的启动参数不一样:bash 常用 -c,登录外壳会用 -lc;PowerShell 要用 -Command,有时还要加 -NoProfile;cmd 则用 /c。这个文件就像“翻译员”,把项目内部想执行的命令,翻译成对应外壳真正能听懂的一串启动参数。它还保存外壳的类型和路径,比如“这是 bash,程序在 /bin/bash”。另外,它会把其他模块检测到的外壳信息转成这里统一的 Shell,并提供一些便捷函数去获取默认用户外壳、按指定路径找外壳、按类型找外壳。没有它,执行命令的代码就会到处散落不同系统的特殊写法,很容易在某个平台上跑错。
Shell::name16–18 ↗
fn name(&self) -> &'static str
作用:返回这个外壳的简单名字,比如 bash、zsh、powershell。别人需要展示当前外壳,或记录环境信息时会用它。
数据流:进去的是一个已经创建好的 Shell,它里面带着外壳类型 → 函数读取这个类型,并询问 ShellType 自己对应的名字 → 出来的是一个固定的文字名字,不改动任何数据。
调用关系:它是 Shell 对象上的一个小查询工具。构建环境更新项时会用它写出外壳名称,执行脚本前也会用它知道当前要通过哪个外壳跑命令。它把具体取名的工作交给 ShellType 的 name 方法。
调用图:调用 1 个内部函数(name);被 2 处调用(build_environment_update_item, run_script_with_timeout)。
Shell::derive_exec_args22–49 ↗
fn derive_exec_args(&self, command: &str, use_login_shell: bool) -> Vec<String>
作用:把一条要执行的命令,变成真正传给系统启动程序的一组参数。它解决的是“同一句命令,在不同外壳里该怎么启动”的问题。
数据流:进去的是 Shell 本身、命令文字,以及是否要用登录外壳的开关 → 函数先看外壳类型,再按不同规则拼参数:bash/zsh/sh 用 -c 或 -lc,PowerShell 用 -Command,非登录时加 -NoProfile,cmd 用 /c → 出来的是一个字符串列表,第一项通常是外壳程序路径,后面是启动选项和命令内容;它不修改 Shell。
调用关系:这是执行命令前的关键翻译步骤。运行带超时的脚本时会调用它,构造基础命令对象时也会调用它。它不负责真正执行命令,只负责把“想跑什么”整理成系统执行接口能接受的参数。
调用图:被 2 处调用(run_script_with_timeout, base_command);外部调用 1 个(vec!)。
Shell::from53–58 ↗
fn from(detected: DetectedShell) -> Self
作用:把外部检测模块找到的 DetectedShell 转成项目内部使用的 Shell。这样后面的代码不用关心检测模块自己的类型。
数据流:进去的是 DetectedShell,里面有外壳类型和外壳路径 → 函数把这两个字段原样拿出来,放进新的 Shell 结构里 → 出来的是统一格式的 Shell,没有额外副作用。
调用关系:它是两个模块之间的适配器。底层的 shell_detect 模块负责发现外壳,这里把发现结果转换成 core 里通用的 Shell,供默认外壳、指定路径外壳、兜底外壳等入口继续使用。
Shell::from_environment_shell_info62–76 ↗
fn from_environment_shell_info(shell_info: ShellInfo) -> anyhow::Result<Self>
作用:把执行服务器传来的环境外壳信息,转换成这里的 Shell。它还会检查外壳名字是不是项目认识的类型,防止后面拿未知外壳去乱拼命令。
数据流:进去的是 ShellInfo,里面有外壳名字和路径 → 函数根据名字匹配成 ShellType,比如 zsh、bash、powershell、sh、cmd;如果名字不认识,就返回错误 → 成功时出来一个 Shell,路径来自 ShellInfo,类型是匹配出来的 ShellType。
调用关系:它通常在解析用户或环境选择时被调用。resolve_selection 会拿到环境里声明的外壳信息,然后交给这个函数做校验和转换。遇到未知名字时,它会直接报错,避免后续执行阶段才出现更难懂的问题。
调用图:被 1 处调用(resolve_selection);外部调用 2 个(from, bail!)。
ultimate_fallback_shell80–82 ↗
fn ultimate_fallback_shell() -> Shell
作用:在测试环境里拿到一个“最后兜底”的外壳。意思是当前面所有正常检测都不可用时,测试可以确认系统还有一个可依赖的默认选择。
数据流:进去没有参数 → 函数调用底层 shell_detect 模块的兜底外壳查找逻辑 → 出来的是转换后的 Shell。
调用关系:这个函数只在 Unix 测试中编译使用。它把真正的兜底检测工作交给 codex_shell_command::shell_detect::ultimate_fallback_shell,然后通过 Shell::from 的转换能力变成当前文件的 Shell 类型。
调用图:调用 1 个内部函数(ultimate_fallback_shell)。
get_shell_by_model_provided_path84–86 ↗
fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> Shell
作用:根据一个外部提供的外壳程序路径,判断它是什么外壳,并包装成 Shell。比如给了一个路径,它会尽量识别这是 bash、cmd 还是别的支持类型。
数据流:进去的是一个路径 PathBuf → 函数把路径交给底层检测模块分析 → 底层返回检测到的外壳信息后,这里转换成 Shell 返回。
调用关系:它是“按路径指定外壳”这条路的入口。测试或配置代码如果想强制使用某个 Windows cmd 路径,会通过它拿到统一 Shell;具体识别路径的细节不在这里做,而是交给 shell_detect 模块。
调用图:调用 1 个内部函数(get_shell_by_model_provided_path);被 1 处调用(with_windows_cmd_shell)。
get_shell88–90 ↗
fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<Shell>
作用:按指定外壳类型和可选路径查找一个可用外壳。有人明确想要 bash、PowerShell 等类型时,会走这个函数。
数据流:进去的是目标 ShellType,以及一个可能存在的路径 → 函数交给底层检测模块去找匹配的外壳;如果找到了,就转换成 Shell;如果找不到,就返回空值 → 它不改动外部状态。
调用关系:它在创建配置或写入外壳快照时会被调用。它扮演的是“查找并包装”的角色:上层提出想要什么,底层负责找,这里负责把结果变成 core 统一使用的 Shell。
调用图:调用 1 个内部函数(get_shell);被 2 处调用(new, write_shell_snapshot)。
default_user_shell92–94 ↗
fn default_user_shell() -> Shell
作用:取得当前用户默认的外壳。很多地方没有特别指定外壳时,就靠它决定命令应该通过谁来运行。
数据流:进去没有参数 → 函数调用底层检测模块,按操作系统和用户环境找默认外壳 → 出来的是统一的 Shell 对象。
调用关系:这是最常用的外壳入口之一。创建会话、解析运行环境、生成当前外壳输出命令、测试默认行为等场景都会调用它。它不自己做复杂检测,而是把检测交给 shell_detect,再把结果转换为本文件的 Shell。
调用图:调用 1 个内部函数(default_user_shell);被 15 处调用(current_shell_output_command, latest_environment_update_wins_while_previous_resolution_is_pending, resolve_turn_environments, new, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, resolved_environments_for_configuration, test_get_command_rejects_explicit_login_when_disallowed, test_get_command_rejects_explicit_shell_in_zsh_fork_mode, test_get_command_respects_explicit_bash_shell (+5 more))。
default_user_shell_from_path97–99 ↗
fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> Shell
作用:在 macOS 测试里,根据给定的用户外壳路径来推导默认外壳。它用于验证“从路径判断默认外壳”的规则是否正确。
数据流:进去的是一个可能存在的路径:有路径就按它判断,没有路径就按底层默认规则处理 → 函数调用底层的 default_user_shell_from_path → 出来的是转换后的 Shell。
调用关系:这个函数只在 macOS 测试中启用。它服务于测试代码,不是日常运行入口;真正的判断逻辑在 codex_shell_command::shell_detect 中,这里只是把结果接到 core 的 Shell 类型上。
调用图:调用 1 个内部函数(default_user_shell_from_path)。
protocol/src/shell_environment.rs源码 ↗
可以把这个文件想成“给子进程打包随身行李”的清单管理员。它先看策略:是继承全部环境变量、一个都不继承,还是只继承最基本的一小批,比如 PATH、HOME、TEMP。接着它会默认过滤掉名字里像 KEY、SECRET、TOKEN 这类可能藏着秘密的变量,除非配置明确说不要这样做。然后再按用户自己的排除规则删一遍,按用户设置的固定值补上或覆盖变量。最后,如果配置了 include_only,它会只保留指定匹配的变量。文件还会在需要时加入 CODEX_THREAD_ID,用来把当前线程或会话标识传给子进程。Windows 上还有一个特别补丁:如果缺少 PATHEXT,就补上常见可执行文件后缀,避免命令查找出问题。
create_env10–15 ↗
fn create_env(
policy: &ShellEnvironmentPolicy,
thread_id: Option<&str>,
) -> HashMap<String, String>
作用:这是最常用的入口:它从当前程序自己的环境变量里取数据,然后按给定策略整理出一份要交给 shell 的新环境变量表。有人要启动子命令时,就会用它来避免把不该传的东西传出去。
数据流:进去的是一份 shell 环境策略,以及可选的 thread_id。它先读取当前进程的所有环境变量,再把这些变量、策略和 thread_id 一起交给 create_env_from_vars。出来的是一个 HashMap,也就是整理好的“变量名到变量值”的表;它本身不直接改系统环境,只返回一份新表。
调用关系:它站在最外层,负责把真实系统环境拿到手,然后把具体筛选工作交给 create_env_from_vars。调用图里显示它会被 create_env 自身和 child_env 使用;实际意义上,child_env 这类启动子进程的地方会在准备命令环境时调用它。
调用图:调用 1 个内部函数(create_env_from_vars);被 2 处调用(create_env, child_env);外部调用 1 个(vars)。
create_env_from_vars17–44 ↗
fn create_env_from_vars(
vars: I,
policy: &ShellEnvironmentPolicy,
thread_id: Option<&str>,
) -> HashMap<String, String>
作用:这个函数适合在已经有一批环境变量时使用,比如测试里手工给一组变量,或某个调用方不想直接读取真实系统环境。它会按策略生成最终环境,并处理 Windows 上缺 PATHEXT 的特殊情况。
数据流:进去的是一批变量、环境策略和可选 thread_id。它先调用 populate_env 做主要过滤、覆盖和补充;如果当前系统是 Windows,它再检查有没有 PATHEXT,大小写不敏感地查,缺了就补上“.COM;.EXE;.BAT;.CMD”。出来的是最终给子进程使用的环境变量表。
调用关系:它是 create_env 和核心整理逻辑 populate_env 之间的中间层。create_env 负责拿真实环境,它负责把变量交给 populate_env,并补上平台相关的小修正;测试 create_env_inserts_pathext_on_windows_when_missing 也直接调用它来确认 Windows 补丁有效。
调用图:调用 1 个内部函数(populate_env);被 4 处调用(create_env_from_vars, create_env, create_env_inserts_pathext_on_windows_when_missing, remote_env_policy_effectively_filters_unrequested_vars);外部调用 1 个(cfg!)。
populate_env46–110 ↗
fn populate_env(
vars: I,
policy: &ShellEnvironmentPolicy,
thread_id: Option<&str>,
) -> HashMap<String, String>
作用:这是这个文件的核心函数,真正按策略决定哪些环境变量留下、哪些删掉、哪些覆盖。它像一道多级安检:先决定带多少,再过滤敏感项,再应用用户规则,最后加上线程标识。
数据流:进去的是一批原始变量、策略和可选 thread_id。它先按 inherit 策略决定起点:全部继承、完全不继承,或只继承系统运行常用的核心变量;接着默认删掉名字匹配 KEY、SECRET、TOKEN 的变量;再应用用户自定义 exclude;然后把 policy.set 里的变量写入,已有的会被覆盖;如果 include_only 不为空,就只保留匹配它的变量;最后有 thread_id 就加入 CODEX_THREAD_ID。出来的是整理后的环境变量表。
调用关系:它被 create_env_from_vars 调用,是整个环境变量筛选流水线的发动机。平台相关的核心变量名单由本文件里的 UNIX_CORE_ENV_VARS 或 WINDOWS_CORE_ENV_VARS 提供;Windows 和非 Windows 的测试都会直接调用它,确认核心变量继承和大小写匹配符合预期。
调用图:被 4 处调用(populate_env, create_env_from_vars, core_inherit_preserves_non_windows_core_vars_case_insensitively, core_inherit_preserves_windows_startup_vars_case_insensitively);外部调用 3 个(new, into_iter, vec!)。
windows_tests::make_vars156–161 ↗
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)>
作用:这是 Windows 测试里的小帮手,把容易阅读的字符串对变成函数需要的变量列表。它存在是为了让测试用例更短、更清楚。
数据流:进去的是一组类似“变量名、变量值”的字符串切片。它逐个把借来的字符串复制成 String,组成 Vec<(String, String)>。出来的是可以直接传给 populate_env 或 create_env_from_vars 的测试数据。
调用关系:它只在 Windows 测试模块里服务测试函数。core_inherit_preserves_windows_startup_vars_case_insensitively 会用它准备模拟环境变量,然后再把数据交给 populate_env。
windows_tests::core_inherit_preserves_windows_startup_vars_case_insensitively165–196 ↗
fn core_inherit_preserves_windows_startup_vars_case_insensitively()
作用:这个测试确认 Windows 下选择“只继承核心变量”时,常见启动所需变量会被保留,而且变量名大小写不同也能认出来。这样可以避免因为 Windows 环境变量大小写习惯不同而误删关键变量。
数据流:进去的是测试里构造的一批变量,包括 Shell、SystemRoot、AppData、TmpDir,以及一个模拟密钥 OPENAI_API_KEY。测试把策略设为 Core,并关闭默认敏感变量过滤,随后调用 populate_env。出来的结果应该只保留 Windows 核心名单里的那几个变量,OPENAI_API_KEY 不在核心名单中,所以不会留下;测试用断言比较实际结果和预期结果。
调用关系:它通过 windows_tests::make_vars 准备输入,再调用 populate_env 检查核心逻辑。它不参与真实运行,只在 Windows 测试时保护 populate_env 的行为不被改坏。
调用图:调用 1 个内部函数(populate_env);外部调用 4 个(default, from, assert_eq!, make_vars)。
windows_tests::create_env_inserts_pathext_on_windows_when_missing200–211 ↗
fn create_env_inserts_pathext_on_windows_when_missing()
作用:这个测试确认 Windows 上即使策略说“不继承任何变量”,create_env_from_vars 也会补上 PATHEXT。PATHEXT 告诉 Windows 哪些后缀算可执行文件,缺了可能导致命令找不到。
数据流:进去的是空变量列表和一个不继承任何变量的策略。测试调用 create_env_from_vars,让它走完整环境生成流程。出来的结果应该只有一个 PATHEXT,值是“.COM;.EXE;.BAT;.CMD”;测试用断言确认这一点。
调用关系:它直接测试 create_env_from_vars,而不是 populate_env,因为 PATHEXT 补丁是在 create_env_from_vars 里做的。它只在 Windows 测试环境中运行,用来防止这个平台修正被意外删除。
调用图:调用 1 个内部函数(create_env_from_vars);外部调用 4 个(default, from, new, assert_eq!)。
non_windows_tests::make_vars219–224 ↗
fn make_vars(pairs: &[(&str, &str)]) -> Vec<(String, String)>
作用:这是非 Windows 测试里的小工具,用来把简单的字符串对转换成环境变量列表。它让测试数据写起来像普通清单,而不是一堆重复的类型转换代码。
数据流:进去的是若干“变量名、变量值”的字符串对。它把每个变量名和值都转成 String,再放进一个 Vec。出来的是 populate_env 可以直接处理的变量列表。
调用关系:它只服务非 Windows 测试模块。core_inherit_preserves_non_windows_core_vars_case_insensitively 用它准备模拟环境,然后把结果交给 populate_env。
non_windows_tests::core_inherit_preserves_non_windows_core_vars_case_insensitively227–249 ↗
fn core_inherit_preserves_non_windows_core_vars_case_insensitively()
作用:这个测试确认在 Linux、macOS 这类非 Windows 系统上,“只继承核心变量”会保留 PATH、HOME、TMPDIR 这类基本变量,并且匹配变量名时不怕大小写差异。
数据流:进去的是测试构造的变量:path、home、TmpDir 和一个 OPENAI_API_KEY。策略设为 Core,并关闭默认敏感变量过滤。测试调用 populate_env 后,结果应保留 path、home、TmpDir,因为它们属于非 Windows 核心变量;OPENAI_API_KEY 不属于核心变量,所以不会出现。最后用断言确认结果和预期一致。
调用关系:它通过 non_windows_tests::make_vars 准备输入,再调用 populate_env 验证核心筛选规则。它只在非 Windows 测试环境里运行,用来保证 Unix 类系统上的启动环境不会被筛得过头。
调用图:调用 1 个内部函数(populate_env);外部调用 4 个(default, from, assert_eq!, make_vars)。
tui/src/exec_command.rs源码 ↗
终端界面经常要显示命令,比如展示“刚才运行了什么”。但命令不是简单拼字符串就行:参数里可能有空格、引号、特殊符号;有些命令外面还套了一层 bash -lc 或 zsh -lc,真正有用的是里面那段脚本;Windows 路径又可能长得像 shell 语法,乱拆会出错。这个文件就是一个“小翻译器”:把参数列表转成安全、可读的命令行;遇到 shell 包装时只显示里面真正执行的脚本;需要拆命令字符串时,会先确认拆完再拼回去不会变味,才放心拆。它还提供把绝对路径转成相对家目录路径的工具,让界面显示更清爽。文件底部的测试是在确认这些容易踩坑的情况不会被改坏。
escape_command8–10 ↗
fn escape_command(command: &[String]) -> String
作用:把一组命令参数拼成一行适合 shell 显示的命令文字。它会给带空格或特殊符号的参数加上合适的引号,避免看起来像另一条命令。
数据流:进去的是一个字符串列表,比如程序名和它的参数;函数先尝试用 shlex 的规则安全拼接,也就是按 shell 能理解的方式加引号;如果拼接失败,就退一步用空格直接连接。出来的是一整行命令字符串,不改动原来的参数列表。
调用关系:它是更高层显示函数的基础零件。strip_bash_lc_and_escape 在发现命令不是 shell 包装时,会把工作交给它来生成可读命令;测试 tests::test_escape_command 会专门检查它对空格和特殊符号的处理。
调用图:被 2 处调用(strip_bash_lc_and_escape, test_escape_command);外部调用 1 个(try_join)。
strip_bash_lc_and_escape12–17 ↗
fn strip_bash_lc_and_escape(command: &[String]) -> String
作用:把命令整理成最适合给人看的形式。如果命令是 bash -lc 或 zsh -lc 这种“外壳套外壳”,它会只拿出里面真正执行的脚本。
数据流:进去的是命令参数列表;函数先检查它是不是类似 bash -lc "echo hello" 的结构。如果是,就直接输出里面的 echo hello;如果不是,就调用 escape_command,把参数安全拼成一行。出来的是用于界面展示或记录的命令文字。
调用关系:它处在“执行命令”和“展示命令”之间,像把后台格式翻译成人话的步骤。build_header、command_display_lines、transcript_lines 会用它来生成标题、命令展示行或转录内容;它自己会根据情况调用 extract_shell_command 或 escape_command。
调用图:调用 2 个内部函数(extract_shell_command, escape_command);被 4 处调用(build_header, command_display_lines, transcript_lines, test_strip_bash_lc_and_escape)。
split_command_string19–33 ↗
fn split_command_string(command: &str) -> Vec<String>
作用:把一整串命令文字尽量拆成参数列表,但只在确认不会误拆时才拆。这样可以避免把 Windows 路径这类看起来像特殊语法的内容弄坏。
数据流:进去的是一整行命令字符串;函数先用 shlex 规则尝试拆分,也就是按常见 shell 引号和空格规则切开。然后它再把拆出来的部分拼回去检查:如果拼回去和原文一致,或者再次拆分仍然能得到同样结果,就返回拆好的列表;如果不可靠,就返回只包含原始整串命令的列表。
调用关系:它主要服务于 command_execution_command_and_parsed,后者需要同时拿到原始命令和比较可信的拆分结果。这个函数自己会用 shlex 的 split 和 try_join 做“拆开再复原”的安全检查。
调用图:被 1 处调用(command_execution_command_and_parsed);外部调用 3 个(split, try_join, vec!)。
relativize_to_home38–51 ↗
fn relativize_to_home(path: P) -> Option<PathBuf>
作用:把位于用户家目录里的绝对路径缩短成相对路径。这样界面上不用总显示 /Users/某人 或 /home/某人 这种很长的前缀。
数据流:进去的是一个路径;函数先确认它是绝对路径,因为相对路径没法判断是否在家目录下。然后它读取当前用户的家目录,再尝试从路径前面去掉家目录这一段;成功就输出剩下的相对路径,失败就输出空值,表示不能缩短。
调用关系:它是路径显示用的小工具。display_path_for 和 format_directory_display 会在准备展示目录或文件位置时调用它,让家目录内的路径更短、更容易读。它依赖系统提供的 home_dir 来知道当前用户的家目录在哪里。
调用图:被 2 处调用(display_path_for, format_directory_display);外部调用 4 个(as_ref, is_absolute, strip_prefix, home_dir)。
tests::test_escape_command58–62 ↗
fn test_escape_command()
作用:检查 escape_command 是否真的会把带空格和特殊符号的参数正确加引号。这样以后有人改代码时,不容易把命令显示弄得含糊或危险。
数据流:进去的是测试里写死的一组参数:foo、bar baz、weird&stuff;测试调用 escape_command 得到一行命令;最后和预期字符串比较。它不改变系统状态,只验证结果是否正确。
调用关系:这是 escape_command 的单元测试,也就是专门盯住一个小函数的测试。测试运行时它会调用 escape_command,并用断言确认输出符合预期。
调用图:调用 1 个内部函数(escape_command);外部调用 2 个(assert_eq!, vec!)。
tests::test_strip_bash_lc_and_escape65–85 ↗
fn test_strip_bash_lc_and_escape()
作用:检查 strip_bash_lc_and_escape 能不能识别 bash、zsh,以及它们的绝对路径形式,并只显示里面真正执行的脚本。
数据流:进去的是几组测试命令参数,比如 bash -lc echo hello、/usr/bin/zsh -lc echo hello;每组都会交给 strip_bash_lc_and_escape;出来的结果都应该是 echo hello。测试通过断言确认外层 shell 被正确去掉。
调用关系:这是 strip_bash_lc_and_escape 的防回归测试,重点保护“去掉 shell 包装”这个行为。它直接调用被测函数,确保 build_header、command_display_lines、transcript_lines 以后拿到的展示文字不会变啰嗦。
调用图:调用 1 个内部函数(strip_bash_lc_and_escape);外部调用 2 个(assert_eq!, vec!)。
tests::split_command_string_round_trips_shell_wrappers88–100 ↗
fn split_command_string_round_trips_shell_wrappers()
作用:检查 split_command_string 对常见 shell 包装命令能安全拆开。这里的重点是:拆开后再拼回去仍然能保持同一个意思。
数据流:测试先构造一条带 /bin/zsh -lc 和 Python 脚本的命令字符串;然后验证拆分结果应该是 shell 路径、-lc、脚本三段。它只是比较结果,不会真的执行命令。
调用关系:这个测试保护 split_command_string 的“可往返才拆分”规则。它用 try_join 生成一条标准命令,再检查拆分函数是否能把这种可靠格式拆成调用方需要的参数列表。
调用图:外部调用 2 个(assert_eq!, try_join)。
tests::split_command_string_preserves_non_roundtrippable_windows_commands103–106 ↗
fn split_command_string_preserves_non_roundtrippable_windows_commands()
作用:检查 split_command_string 不会乱拆 Windows 风格路径命令。因为 C:\Program Files 这类路径很容易被按空格或反斜杠误解。
数据流:进去的是一整条 Windows 风格的 bash.exe 命令字符串;测试期望输出仍然是只包含原字符串的列表。也就是说,函数发现拆分不可靠时应该选择保守处理,保留原样。
调用关系:这个测试专门守住跨平台兼容性。它确保 command_execution_command_and_parsed 在遇到 Windows 命令时,不会拿到被错误切碎的参数。
调用图:外部调用 1 个(assert_eq!)。
Git 操作和仓库检查
本组从 git-utils 公共接口出发,经由安全的底层 git 调用,构建 fsmonitor 策略、分支逻辑、仓库元数据收集和面向用户的 diff 生成。
git-utils/src/lib.rs源码 ↗
可以把这个文件理解成一家商店的总柜台。真正干活的东西放在 apply、baseline、info、branch 等内部模块里,而这里负责把常用能力公开出去:比如应用 Git 补丁、读取仓库信息、找当前分支、比较远端差异、重置仓库、创建符号链接等。没有这个文件,外部代码就得直接去找每个内部模块,既麻烦,也会把内部结构暴露得太多。这里用 Rust 的“模块”和“重新导出”机制:模块就是把代码按主题分房间;重新导出就是把房间里的常用工具放到大厅货架上。它本身不实现具体 Git 操作,但决定了这个库对外能提供哪些入口,所以是这个工具库的公共接口清单。
git-utils/src/operations.rs源码 ↗
这个文件解决的是一个很实际的问题:程序需要问 Git 一些事,比如“这里是不是 Git 仓库”“当前提交是哪一个”“仓库根目录在哪里”,也需要执行一些 Git 操作。如果每个地方都自己调用命令行,很容易漏掉错误处理,或者被用户配置的 Git hook(Git 在某些操作前后自动运行的小脚本)干扰。这里统一用 run_git 启动外部的 git 程序,并且临时把 hook 关掉,就像办事时走一条不受私人规则影响的专用通道。上层函数再按需求分成两类:只关心成功失败的用 run_git_for_status,需要读取文字结果的用 run_git_for_stdout。另外还有几个更具体的小工具,用来确认仓库、解析 HEAD(当前提交指针)和找到仓库根目录。它的重要点在于:所有 Git 命令失败、输出不是 UTF-8 文本、路径不是仓库等情况,都会被翻译成统一的 GitToolingError,方便上层可靠处理。
ensure_git_repository11–31 ↗
fn ensure_git_repository(path: &Path) -> Result<(), GitToolingError>
作用:检查给定目录是不是一个 Git 工作区,也就是能正常被 Git 管的目录。别人会在真正做 Git 相关计算前先调用它,避免后面才发现“这里根本不是仓库”。
数据流:进去的是一个路径。它在这个路径下运行 git rev-parse --is-inside-work-tree,读取 Git 打印出来的文字;如果文字是 true,就什么也不返回错误,表示检查通过;如果 Git 明确说不行,或者返回码是 128,就把这个路径包装成“不是 Git 仓库”的错误;其他异常原样往外交。
调用关系:它是更高层流程的门卫。merge_base_with_head 在计算分支和当前提交的共同基础前会先找它确认环境;它自己不直接碰 Git 细节,而是把实际执行命令的活交给 run_git_for_stdout。
调用图:调用 1 个内部函数(run_git_for_stdout);被 1 处调用(merge_base_with_head);外部调用 2 个(to_path_buf, vec!)。
resolve_head33–47 ↗
fn resolve_head(path: &Path) -> Result<Option<String>, GitToolingError>
作用:尝试找出当前仓库的 HEAD,也就是当前检出的提交编号。它特别照顾“仓库还没有任何提交”的情况,这时不会当成严重错误,而是返回“没有”。
数据流:进去的是一个目录路径。它运行 git rev-parse --verify HEAD,如果成功,就拿到 Git 输出的提交哈希字符串并放进 Some;如果 Git 返回 128,通常表示 HEAD 不存在,于是返回 None;如果是别的执行错误,就返回对应错误。
调用关系:merge_base_with_head 需要知道当前提交时会调用它。它把执行 Git 命令和读取输出的细节交给 run_git_for_stdout,自己只负责把“有提交”和“没提交”这两种情况说清楚。
调用图:调用 1 个内部函数(run_git_for_stdout);被 1 处调用(merge_base_with_head);外部调用 1 个(vec!)。
resolve_repository_root49–59 ↗
fn resolve_repository_root(path: &Path) -> Result<PathBuf, GitToolingError>
作用:找到一个 Git 仓库的根目录。哪怕用户传进来的是仓库里的子文件夹,它也能问 Git 真正的顶层目录在哪里。
数据流:进去的是一个路径。它运行 git rev-parse --show-toplevel,拿到 Git 输出的根目录文字,再把这段文字变成 PathBuf,也就是程序里可继续使用的路径对象;如果命令失败,就返回错误。
调用关系:merge_base_with_head 在需要以仓库整体为单位做判断时会用它。它依赖 run_git_for_stdout 获取 Git 的文字回答,然后只做一层简单的路径转换。
调用图:调用 1 个内部函数(run_git_for_stdout);被 1 处调用(merge_base_with_head);外部调用 2 个(from, vec!)。
run_git_for_status61–72 ↗
fn run_git_for_status(
dir: &Path,
args: I,
env: Option<&[(OsString, OsString)]>,
) -> Result<(), GitToolingError>
作用:执行一个 Git 命令,但只关心它有没有成功,不关心它输出了什么。适合用在“做一件事就行”的场景,比如更新索引。
数据流:进去的是工作目录、Git 参数列表,以及可选的环境变量。它把这些交给 run_git 真正运行;如果命令成功,它丢掉输出并返回成功;如果失败,就把 run_git 给出的错误继续返回。
调用关系:write_index_from_head 会调用它来执行只看成败的 Git 操作。它位于上层业务和底层命令执行之间,是一个“简化版接口”,底层活都交给 run_git。
调用图:调用 1 个内部函数(run_git);被 1 处调用(write_index_from_head)。
run_git_for_stdout74–90 ↗
fn run_git_for_stdout(
dir: &Path,
args: I,
env: Option<&[(OsString, OsString)]>,
) -> Result<String, GitToolingError>
作用:执行一个 Git 命令,并把标准输出当作文本取回来。它适合用在需要 Git 回答一个值的场景,比如提交编号、分支引用、仓库根目录。
数据流:进去的是目录、参数和可选环境变量。它先调用 run_git 得到命令执行结果,再把标准输出从字节转换成 UTF-8 字符串(UTF-8 是常见文本编码);转换成功后会去掉首尾空白再返回;如果输出不是合法文本,就返回专门的输出编码错误。
调用关系:它是很多查询类 Git 操作的共同入口,merge_base_with_head、resolve_branch_ref、resolve_upstream_if_remote_ahead、ensure_git_repository、resolve_head 和 resolve_repository_root 都会用它。它自己负责“把命令结果变成干净文字”,真正启动进程则交给 run_git。
调用图:调用 1 个内部函数(run_git);被 6 处调用(merge_base_with_head, resolve_branch_ref, resolve_upstream_if_remote_ahead, ensure_git_repository, resolve_head, resolve_repository_root);外部调用 1 个(from_utf8)。
run_git92–134 ↗
fn run_git(
dir: &Path,
args: I,
env: Option<&[(OsString, OsString)]>,
) -> Result<GitRun, GitToolingError>
作用:这是本文件最底层的 Git 执行器,真正启动系统里的 git 程序。它统一加上禁用 hook 的配置、设置工作目录和环境变量,并把失败信息整理成项目自己的错误。
数据流:进去的是运行目录、Git 参数和可选环境变量。它先在参数前加上 -c core.hooksPath=...,意思是这次 Git 命令不要运行用户配置的 hook;然后拼出一段可读的命令字符串用于报错;接着创建 git 进程,设置当前目录、环境变量和参数,运行后拿到输出;如果退出状态表示失败,它会读取错误输出并生成 GitToolingError::GitCommand;如果成功,就返回 GitRun,里面包含命令字符串和完整输出。
调用关系:它是所有 Git 调用的发动机。run_git_for_status 和 run_git_for_stdout 都把实际运行命令的工作交给它;它为了让错误更好懂,会调用 build_command_string 生成类似命令行里看到的 git ... 文本。
调用图:调用 1 个内部函数(build_command_string);被 2 处调用(run_git_for_status, run_git_for_stdout);外部调用 6 个(into_iter, from, from_utf8_lossy, with_capacity, new, format!)。
build_command_string136–146 ↗
fn build_command_string(args: &[OsString]) -> String
作用:把一组 Git 参数拼成一段人能看懂的命令文字,主要用于错误消息。这样出错时,读者能知道程序当时大概运行了哪条 Git 命令。
数据流:进去的是一组参数。如果参数为空,它返回 git;如果有参数,它把每个参数转成可显示的文字,用空格连起来,再在前面加上 git ,输出一条完整的命令字符串。它不执行任何命令,也不改动外部状态。
调用关系:它只被 run_git 使用。run_git 在真正启动 Git 前先让它生成命令说明,等命令失败时就能把这段说明放进错误里,帮助上层和用户定位问题。
git-utils/src/fsmonitor.rs源码 ↗
Git 有个 core.fsmonitor 配置,可以让 Git 少扫很多文件,速度更快。但这个配置也可能指向一个外部程序;如果仓库自己指定了这个程序,Codex 直接照做就有安全风险。这个文件像一个门卫:先问 Git 当前 core.fsmonitor 到底是什么值,再判断它是不是“真的开启”。如果是关闭、格式奇怪、包含空字节、不是 UTF-8 文本,或者 Git 命令查询失败,就一律关掉。只有在配置明确等价于 true,并且 Git 自己声明支持内置 fsmonitor--daemon(文件监控后台服务)时,才允许保留。最后它把决定变成 Git 命令可直接使用的配置参数,比如 core.fsmonitor=false 或 true。
FsmonitorOverride::git_config_arg24–29 ↗
fn git_config_arg(self) -> &'static str
作用:把“禁用”或“保留内置监控”这个决定,转换成可以传给 Git 的配置字符串。别人调 Git 命令时用它,就不用自己拼 core.fsmonitor 这种参数。
数据流:进去的是一个 FsmonitorOverride 值,也就是前面已经做好的选择;函数只看这个选择:如果是 Disabled,就给出 "core.fsmonitor=false",如果是 BuiltIn,就给出 "core.fsmonitor=true";出来的是一段固定文本,不改动任何外部状态。
调用关系:它是最后一步的小翻译器。run_git_command_with_timeout_from、run_git_command 和 git_command 在真正组装 Git 命令时会调用它,把安全策略变成 Git 能看懂的命令行配置。
调用图:被 3 处调用(run_git_command_with_timeout_from, run_git_command, git_command)。
detect_fsmonitor_override49–125 ↗
async fn detect_fsmonitor_override(
runner: &mut impl FsmonitorProbeRunner,
) -> FsmonitorOverride
作用:实际判断当前仓库能不能安全使用 Git 内置的文件监控。它宁可保守地关闭,也不会冒险接受仓库指定的外部监控程序。
数据流:进去的是一个 runner,也就是“帮我跑几条 Git 探测命令的人”。函数先让它查询 core.fsmonitor 的原始有效值;如果查询失败、结果不是以空字符结尾、里面混入空字符、或者不是 UTF-8 文本,就直接返回 Disabled。接着它判断这个值是否等价于 true:常见写法直接认,不常见写法再请 Git 按布尔值规则规范化。若不是开启,返回 Disabled。最后它再查询 Git 的构建能力,只有看到 feature: fsmonitor--daemon 这一行,才返回 BuiltIn;否则仍返回 Disabled。
调用关系:它是这套判断流程的核心入口。它把具体执行 Git 探测命令的工作交给 FsmonitorProbeRunner 的 run_probe;判断文本时会用 from_utf8 把字节转成字符串,也用 matches! 做简单结果匹配。它产出的 FsmonitorOverride 后续会交给 FsmonitorOverride::git_config_arg,变成真正传给 Git 的配置参数。
git-utils/src/branch.rs源码 ↗
这个文件解决的是一个很常见的 Git 问题:当前代码和目标分支到底从哪个提交开始分开的?这就像两条岔路,要先找到最后一个共同路口,后面才能判断各自走了多远。核心函数 merge_base_with_head 会先确认给的目录真的是 Git 仓库,再找到仓库根目录和当前 HEAD(HEAD 可以理解为“当前所在提交”)。如果仓库还没有提交,或者目标分支找不到,它会返回“没有结果”,而不是把错误抛给用户。它还做了一个贴心处理:如果本地分支背后的远程分支更新,它会优先拿远程分支来算,避免用过时的本地信息。文件后半部分是测试代码,会临时创建 Git 仓库、提交文件、造出分支和远程仓库,确认这些边界情况都按预期工作。
merge_base_with_head15–48 ↗
fn merge_base_with_head(
repo_path: &Path,
branch: &str,
) -> Result<Option<String>, GitToolingError>
作用:找出当前 HEAD 和指定分支的共同祖先提交。上层代码想知道“当前改动是从目标分支的哪里分出来的”时,就会用它。
数据流:输入是一个仓库路径和一个分支名。它先确认路径是 Git 仓库,再解析出仓库根目录和当前 HEAD;如果没有 HEAD,就返回 None。接着它尝试把分支名解析成真实提交,如果分支不存在也返回 None。然后它检查这个分支有没有远程上游,并且远程是否比本地更新;如果远程更新,就优先用远程分支。最后运行 git merge-base,输出共同祖先提交的哈希字符串,包装成 Some 返回。
调用关系:这是这个文件的主入口。测试里的三个场景都会调用它。它自己把具体小活分给 resolve_branch_ref、resolve_upstream_if_remote_ahead,以及外部的 ensure_git_repository、resolve_repository_root、resolve_head、run_git_for_stdout;也就是说,它像一个总调度员,把“确认仓库、找 HEAD、找分支、算共同祖先”串起来。
调用图:调用 6 个内部函数(resolve_branch_ref, resolve_upstream_if_remote_ahead, ensure_git_repository, resolve_head, resolve_repository_root, run_git_for_stdout);被 3 处调用(merge_base_prefers_upstream_when_remote_ahead, merge_base_returns_none_when_branch_missing, merge_base_returns_shared_commit);外部调用 1 个(vec!)。
resolve_branch_ref50–66 ↗
fn resolve_branch_ref(repo_root: &Path, branch: &str) -> Result<Option<String>, GitToolingError>
作用:把用户给的分支名变成 Git 能确定的提交引用。分支不存在时,它不会当成严重错误,而是温和地告诉调用者“找不到”。
数据流:输入是仓库根目录和分支名。它运行 git rev-parse --verify 分支名;成功时拿到对应的提交或引用字符串,返回 Some。 如果 Git 命令明确失败,比如分支不存在,就返回 None。 如果是其他系统级错误,比如执行命令出问题,就把错误继续交出去。
调用关系:它只被 merge_base_with_head 调用。merge_base_with_head 在计算共同祖先前,需要先确认目标分支到底指向哪里;这个函数就是负责“查分支是否存在、存在就拿到地址”的小零件。
调用图:调用 1 个内部函数(run_git_for_stdout);被 1 处调用(merge_base_with_head);外部调用 1 个(vec!)。
resolve_upstream_if_remote_ahead68–117 ↗
fn resolve_upstream_if_remote_ahead(
repo_root: &Path,
branch: &str,
) -> Result<Option<String>, GitToolingError>
作用:判断指定分支的远程上游分支是否比本地更新。如果远程有本地没有的新提交,它就返回远程分支名,让后续比较用更新的那份。
数据流:输入是仓库根目录和分支名。它先运行 Git 命令查这个分支有没有 upstream(上游分支,通常就是远程服务器上的对应分支)。没有上游或查不到,就返回 None。查到后,它再运行 git rev-list --left-right --count 比较本地分支和上游分支两边各有多少独有提交。右边数量大于 0,说明远程领先,就返回 Some(upstream);否则返回 None。
调用关系:它被 merge_base_with_head 用来避免拿过时的本地分支做比较。merge_base_with_head 会先解析本地分支,再问它“远程是不是更新”;如果是,就改用远程分支算 merge-base。
调用图:调用 1 个内部函数(run_git_for_stdout);被 1 处调用(merge_base_with_head);外部调用 1 个(vec!)。
tests::run_git_in128–135 ↗
fn run_git_in(repo_path: &Path, args: &[&str])
作用:这是测试用的小工具,用来在指定目录里执行一条 Git 命令。它让测试代码不用一遍遍写启动 git 进程的重复代码。
数据流:输入是仓库路径和一组 Git 参数。它创建一个 git 命令进程,把工作目录切到给定路径,传入参数并执行。执行成功就什么也不返回;如果命令失败,测试会立刻断言失败,提示是哪条 Git 命令出错。
调用关系:测试里的初始化仓库、切分支、添加文件、推送远程等操作都靠它完成。tests::init_test_repo 和 tests::commit 也会借它来做更高层的小步骤。
tests::run_git_stdout137–145 ↗
fn run_git_stdout(repo_path: &Path, args: &[&str]) -> String
作用:这是测试用的小工具,用来运行 Git 命令并拿到它打印出来的文字结果。测试用它算出 Git 官方命令的结果,再和本文件函数的结果对比。
数据流:输入是仓库路径和 Git 参数。它在该目录执行 git 命令,确认命令成功后,把标准输出从字节转成字符串,去掉首尾空白,再返回这个字符串。命令失败时,测试直接失败。
调用关系:测试函数用它来取得标准答案,比如直接运行 git merge-base 的输出。然后测试再调用 merge_base_with_head,看两边结果是否一致。
调用图:外部调用 3 个(from_utf8_lossy, assert!, new)。
tests::init_test_repo147–150 ↗
fn init_test_repo(repo_path: &Path)
作用:这是测试用的初始化步骤,用来把一个临时目录变成干净的 Git 仓库。它还关闭自动换行转换,避免不同系统上的测试结果不一致。
数据流:输入是一个目录路径。它先运行 git init --initial-branch=main 创建仓库并指定主分支名,再运行 git config core.autocrlf false 关闭自动换行处理。它不返回数据,只改变这个目录里的 Git 状态。
调用关系:多个测试在开始造提交和分支前都会调用它。它内部把真正的 Git 命令执行交给 tests::run_git_in。
调用图:外部调用 1 个(run_git_in)。
tests::commit152–165 ↗
fn commit(repo_path: &Path, message: &str)
作用:这是测试用的提交工具,用固定的测试用户名和邮箱创建一次 Git 提交。这样测试不用依赖开发者电脑上有没有配置 Git 用户信息。
数据流:输入是仓库路径和提交说明文字。它运行 git commit,并临时带上 user.name 和 user.email 配置。执行后,仓库里会多出一个提交;函数本身不返回提交号。
调用关系:各个测试在写入文件并 git add 之后会调用它。它内部仍然通过 tests::run_git_in 执行真正的 Git 命令。
调用图:外部调用 1 个(run_git_in)。
tests::merge_base_prefers_upstream_when_remote_ahead197–239 ↗
fn merge_base_prefers_upstream_when_remote_ahead() -> Result<(), GitToolingError>
作用:这个测试确认:当远程分支比本地分支更新时,merge_base_with_head 会优先用远程分支来算共同祖先。这样可以避免拿旧的本地 main 得到错误判断。
数据流:它创建一个普通仓库和一个裸仓库(裸仓库可以理解为没有工作区、专门当远程服务器用的仓库)。先提交并推送 main,再创建 feature。接着它把本地 main 改写成另一条历史,但仍设置 upstream 指向 origin/main,然后切回 feature 并 fetch 更新远程信息。最后它用 git merge-base HEAD origin/main 得到标准答案,再检查 merge_base_with_head(repo, "main") 是否返回这个远程版本的答案。
调用关系:这是专门验证 merge_base_with_head 里“远程领先时优先远程”的分支逻辑。它间接覆盖了 resolve_upstream_if_remote_ahead,也使用 tests::run_git_in、tests::commit、tests::run_git_stdout 等测试辅助函数。
调用图:调用 1 个内部函数(merge_base_with_head);外部调用 7 个(assert_eq!, commit, run_git_in, run_git_stdout, create_dir_all, write, tempdir)。
tests::merge_base_returns_none_when_branch_missing242–255 ↗
fn merge_base_returns_none_when_branch_missing() -> Result<(), GitToolingError>
作用:这个测试确认目标分支不存在时,merge_base_with_head 不会报硬错误,而是返回 None。这样调用者可以把它当成“没有可比较对象”来处理。
数据流:它创建临时 Git 仓库,写入并提交一个文件,保证仓库已经有 HEAD。然后它调用 merge_base_with_head(repo, "missing-branch"),传入一个不存在的分支名,最后检查结果是 None。
调用关系:这是对 merge_base_with_head 的缺失分支场景测试。它主要验证 resolve_branch_ref 找不到分支时会被主函数转换成 None,而不是让测试失败或抛出 GitCommand 错误。
调用图:调用 1 个内部函数(merge_base_with_head);外部调用 6 个(assert_eq!, commit, init_test_repo, run_git_in, write, tempdir)。
git-utils/src/info.rs源码 ↗
这个文件像一个 Git 仓库的“问询台”。程序给它一个目录,它先判断这里是不是 Git 仓库,再通过执行 git 命令拿到常用信息,比如当前提交号、分支名、远端地址、最近提交、本地分支和是否有未提交改动。为了防止大仓库或异常 Git 配置把程序卡住,所有 Git 命令都有 5 秒超时;同时它会关闭仓库自带的 hooks(Git 钩子,运行 Git 时可能自动触发的脚本),避免查询信息时被项目里的脚本影响。它还特别处理 fsmonitor(Git 用来加速文件变化检测的机制),尽量保留安全的内置加速,禁用可能来自外部程序的 helper。另一个重要点是 worktree(一个仓库多个工作目录)场景:信任检查时它会顺着 .git 文件找到主仓库根目录,而不是只看当前工作目录。
get_git_repo_root33–40 ↗
fn get_git_repo_root(base_dir: &Path) -> Option<PathBuf>
作用:从一个本地路径开始,往上找最近的 .git 文件或目录,用来确认这个路径属于哪个 Git 仓库。有人会用它来判断“这里是不是项目根目录的一部分”。
数据流:输入一个路径 → 如果它是文件就先退到父目录,如果是目录就从它开始 → 一层层往上找 .git → 找到就返回仓库根目录,找不到就返回空。
调用关系:它把真正的向上查找交给 find_ancestor_git_entry。git_diff_to_remote 在计算本地和远端差异前会先调用它,确保当前目录确实在 Git 仓库里。
调用图:调用 1 个内部函数(find_ancestor_git_entry);被 1 处调用(git_diff_to_remote);外部调用 2 个(is_dir, parent)。
get_git_repo_root_with_fs46–58 ↗
async fn get_git_repo_root_with_fs(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
) -> Option<AbsolutePathBuf>
作用:这是 get_git_repo_root 的“可替换文件系统”版本,不一定只看本机磁盘,也能看远程或沙箱里的文件系统。它用于在非本地环境里找 Git 仓库根目录。
数据流:输入一个文件系统接口和当前绝对路径 → 先询问这个路径是不是目录 → 选定起点后往上检查每层有没有 .git → 返回找到的仓库根目录,失败则返回空。
调用关系:它依赖 find_ancestor_git_entry_with_fs 做逐层查找。resolve_root_git_project_for_trust 会用它先找到当前工作区的 Git 根,再进一步判断真正应该信任的主仓库根。
调用图:调用 3 个内部函数(find_ancestor_git_entry_with_fs, parent, from_abs_path);被 1 处调用(resolve_root_git_project_for_trust);外部调用 2 个(get_metadata, clone)。
collect_git_info87–139 ↗
async fn collect_git_info(cwd: &Path) -> Option<GitInfo>
作用:收集当前仓库的三件基础信息:当前提交号、当前分支名、远端 origin 地址。它适合给界面、日志或上报信息使用。
数据流:输入工作目录 → 先运行 git rev-parse --git-dir 确认是仓库 → 并行运行三个 git 命令读取提交、分支和远端地址 → 把成功读到的字段装进 GitInfo,失败的字段留空。
调用关系:它反复通过 run_git_command_with_timeout 执行 Git 命令,并用并行等待缩短耗时。它不调用更复杂的差异逻辑,只负责轻量汇总仓库身份信息。
调用图:调用 2 个内部函数(run_git_command_with_timeout, new);外部调用 2 个(from_utf8, join!)。
get_git_remote_urls142–152 ↗
async fn get_git_remote_urls(cwd: &Path) -> Option<BTreeMap<String, String>>
作用:读取当前仓库配置过的远端地址,比如 origin 对应哪个网址。它先确认目录真的是 Git 仓库,避免对普通目录乱跑 Git 命令。
数据流:输入工作目录 → 先用 Git 检查仓库存在 → 如果不是仓库返回空 → 如果是仓库,就继续读取远端列表并整理成“名字到地址”的表。
调用关系:它先调用 run_git_command_with_timeout 做仓库检查,之后把具体读取工作交给 get_git_remote_urls_assume_git_repo。
调用图:调用 2 个内部函数(get_git_remote_urls_assume_git_repo, run_git_command_with_timeout)。
get_git_remote_urls_assume_git_repo155–163 ↗
async fn get_git_remote_urls_assume_git_repo(cwd: &Path) -> Option<BTreeMap<String, String>>
作用:在已经确定是 Git 仓库的前提下,读取所有 fetch 用的远端地址。它省掉仓库检查,适合内部流程中已知安全的场景。
数据流:输入工作目录 → 执行 git remote -v → 把输出文本转成字符串 → 交给 parse_git_remote_urls 挑出 fetch 行 → 返回远端名称和地址的映射表。
调用关系:get_git_remote_urls 会在完成仓库检查后调用它。它自己只负责取原始输出,解析细节交给 parse_git_remote_urls。
调用图:调用 2 个内部函数(parse_git_remote_urls, run_git_command_with_timeout);被 1 处调用(get_git_remote_urls);外部调用 1 个(from_utf8)。
get_head_commit_hash166–179 ↗
async fn get_head_commit_hash(cwd: &Path) -> Option<GitSha>
作用:读取当前 HEAD 的提交号。HEAD 可以简单理解为“当前工作目录指向的那次提交”。
数据流:输入工作目录 → 执行 git rev-parse HEAD → 如果命令成功且输出不是空 → 把输出包装成 GitSha 返回;否则返回空。
调用关系:它使用 run_git_command_with_timeout 执行 Git 命令,是一个独立的小查询函数,不依赖分支或远端逻辑。
调用图:调用 2 个内部函数(run_git_command_with_timeout, new);外部调用 1 个(from_utf8)。
canonicalize_git_remote_url181–197 ↗
fn canonicalize_git_remote_url(url: &str) -> Option<String>
作用:把各种写法的 Git 远端地址整理成统一格式,方便比较两个地址是不是指向同一个仓库。比如 GitHub 地址会统一成 github.com/owner/repo 的形式。
数据流:输入一个远端地址字符串 → 去掉空白、末尾斜杠和 .git 后缀 → 判断它是 URL 形式、SSH 的 scp 形式,还是 host/path 形式 → 统一主机和仓库路径 → 返回规范化结果,无法识别就返回空。
调用关系:它是规范化入口,会根据地址形状分别调用 canonicalize_git_url_like_remote、parse_scp_like_remote 和 canonicalize_git_remote_host_path,并复用 trim_git_suffix 去掉 .git 后缀。
调用图:调用 4 个内部函数(canonicalize_git_remote_host_path, canonicalize_git_url_like_remote, parse_scp_like_remote, trim_git_suffix)。
canonicalize_git_url_like_remote199–213 ↗
fn canonicalize_git_url_like_remote(scheme: &str, rest: &str) -> Option<String>
作用:处理带协议头的远端地址,比如 https://、ssh://、git://。它会识别默认端口,让同一个地址的不同写法能对上。
数据流:输入协议名和协议后面的内容 → 根据协议确定默认端口 → 去掉问号或井号后面的附加部分 → 拆出主机和路径 → 交给统一的主机路径整理函数。
调用关系:它只由 canonicalize_git_remote_url 调用,是处理 URL 风格远端地址的分支;最后把活交给 canonicalize_git_remote_host_path 完成统一格式。
调用图:调用 1 个内部函数(canonicalize_git_remote_host_path);被 1 处调用(canonicalize_git_remote_url)。
parse_scp_like_remote215–229 ↗
fn parse_scp_like_remote(remote: &str) -> Option<(&str, &str)>
作用:识别 Git 常见的 SSH 简写地址,比如 git@github.com:owner/repo.git。这种写法看起来不像普通网址,所以需要单独拆。
数据流:输入一个远端地址 → 判断冒号是不是 SSH 简写里的分隔符,而不是普通路径的一部分 → 拆成主机部分和仓库路径 → 合法就返回两段,否则返回空。
调用关系:canonicalize_git_remote_url 在遇到非 URL 地址时会调用它。如果它成功拆开,后续会交给 canonicalize_git_remote_host_path 做统一整理。
调用图:被 1 处调用(canonicalize_git_remote_url)。
canonicalize_git_remote_host_path231–266 ↗
fn canonicalize_git_remote_host_path(
host_part: &str,
path: &str,
default_port: Option<&str>,
) -> Option<String>
作用:把“主机 + 仓库路径”整理成最终可比较的远端仓库标识。它会去掉用户名、默认端口、多余斜杠和 .git 后缀。
数据流:输入主机部分、路径部分和可选默认端口 → 清理主机名并移除用户名 → 清理路径并确认至少有 owner/repo 两段 → GitHub 地址会把路径转小写 → 返回统一字符串。
调用关系:canonicalize_git_remote_url 和 canonicalize_git_url_like_remote 都会把拆好的内容交给它。它内部调用 normalize_remote_host 和 trim_git_suffix,是远端地址规范化的核心步骤。
调用图:调用 2 个内部函数(normalize_remote_host, trim_git_suffix);被 2 处调用(canonicalize_git_remote_url, canonicalize_git_url_like_remote);外部调用 2 个(format!, matches!)。
normalize_remote_host268–277 ↗
fn normalize_remote_host(host: &str, default_port: Option<&str>) -> String
作用:统一远端地址里的主机名。它会把主机名转小写,并在端口是协议默认端口时把端口去掉。
数据流:输入主机字符串和默认端口 → 主机名转小写 → 如果末尾端口等于默认端口,就去掉端口 → 输出清理后的主机名。
调用关系:它由 canonicalize_git_remote_host_path 调用,专门处理主机这一小块,避免主流程里混太多细节。
调用图:被 1 处调用(canonicalize_git_remote_host_path)。
trim_git_suffix279–281 ↗
fn trim_git_suffix(value: &str) -> &str
作用:去掉字符串末尾的 .git。很多远端地址有这个后缀,但比较仓库身份时通常不希望它造成差异。
数据流:输入一个字符串 → 如果以 .git 结尾就去掉 → 否则原样返回。
调用关系:canonicalize_git_remote_url 和 canonicalize_git_remote_host_path 都会调用它,用来让远端地址和路径更容易互相比较。
调用图:被 2 处调用(canonicalize_git_remote_host_path, canonicalize_git_remote_url)。
get_has_changes283–293 ↗
async fn get_has_changes(cwd: &Path) -> Option<bool>
作用:判断当前工作目录有没有未提交的改动。它相当于问 Git:“工作区干净吗?”
数据流:输入工作目录 → 先检测 fsmonitor 应该怎么设置 → 执行 git status --porcelain → 如果命令成功,就看输出是否为空 → 有输出表示有改动。
调用关系:它先调用 detect_local_fsmonitor_override 决定安全的 fsmonitor 配置,再通过 run_git_command_with_timeout_from 运行实际命令。
调用图:调用 2 个内部函数(detect_local_fsmonitor_override, run_git_command_with_timeout_from);外部调用 1 个(new)。
parse_git_remote_urls295–320 ↗
fn parse_git_remote_urls(stdout: &str) -> Option<BTreeMap<String, String>>
作用:解析 git remote -v 的文本输出,只保留 fetch 用的远端地址。fetch 可以理解为“从远端拉取代码时用的地址”。
数据流:输入多行文本 → 逐行寻找以“(fetch)”结尾的行 → 拆出远端名字和 URL → 填进有序表 → 如果没有任何结果就返回空。
调用关系:get_git_remote_urls_assume_git_repo 负责拿到原始文本后,会调用它做干净的结构化解析。
调用图:被 1 处调用(get_git_remote_urls_assume_git_repo);外部调用 1 个(new)。
recent_commits335–379 ↗
async fn recent_commits(cwd: &Path, limit: usize) -> Vec<CommitLogEntry>
作用:读取当前分支最近的若干条提交,用于提交选择器或历史列表。每条只保留提交号、时间和标题,足够展示给用户看。
数据流:输入工作目录和数量上限 → 先确认是 Git 仓库 → 执行 git log,要求 Git 用特殊分隔符输出提交号、时间、标题 → 逐行解析成 CommitLogEntry 列表 → 失败时返回空列表。
调用关系:它只通过 run_git_command_with_timeout 和 Git 交互,不参与差异计算。它的输出是轻量列表,适合界面层直接展示。
调用图:调用 1 个内部函数(run_git_command_with_timeout);外部调用 4 个(from_utf8_lossy, new, format!, vec!)。
git_diff_to_remote382–394 ↗
async fn git_diff_to_remote(cwd: &Path) -> Option<GitDiffToRemote>
作用:找出本地代码相对最近远端基准提交的完整差异。它用于回答“我现在这份代码,和远端已有代码相比改了什么”。
数据流:输入工作目录 → 先确认在 Git 仓库中 → 读取远端列表 → 推断候选分支链 → 找到离 HEAD 最近且存在于远端的提交号 → 生成相对这个提交的 diff 文本 → 返回基准提交和差异。
调用关系:它是差异计算的总调度者,依次调用 get_git_repo_root、get_git_remotes、branch_ancestry、find_closest_sha 和 diff_against_sha。
调用图:调用 5 个内部函数(branch_ancestry, diff_against_sha, find_closest_sha, get_git_remotes, get_git_repo_root)。
run_git_command_with_timeout397–407 ↗
async fn run_git_command_with_timeout(args: &[&str], cwd: &Path) -> Option<std::process::Output>
作用:用统一方式运行一个 git 命令,并限制最多等 5 秒。这样即使仓库很大或 Git 卡住,主程序也不会一直僵死。
数据流:输入 Git 参数和工作目录 → 默认使用系统里的 git → 禁用 fsmonitor 覆盖 → 交给更底层的命令执行函数 → 返回进程输出或空。
调用关系:很多查询函数都会调用它,比如 collect_git_info、branch_ancestry、get_default_branch 和 current_branch_name。它把公共执行规则集中交给 run_git_command_with_timeout_from。
调用图:调用 1 个内部函数(run_git_command_with_timeout_from);被 12 处调用(branch_ancestry, branch_remote_and_distance, collect_git_info, current_branch_name, get_default_branch, get_default_branch_local, get_git_remote_urls, get_git_remote_urls_assume_git_repo, get_git_remotes, get_head_commit_hash (+2 more));外部调用 1 个(new)。
LocalFsmonitorProbeRunner::run_probe415–424 ↗
async fn run_probe(&mut self, args: &[&str]) -> Option<Vec<u8>>
作用:为本地 Git 提供一个小探针,用来询问 fsmonitor 配置或能力。探针只做很快的元信息查询,不扫描工作区。
数据流:输入一组 Git 参数 → 用指定 git 程序和工作目录启动命令 → 最多等待 5 秒 → 命令成功就返回标准输出的字节,否则返回空。
调用关系:detect_local_fsmonitor_override 创建 LocalFsmonitorProbeRunner 后,会把它交给通用的 detect_fsmonitor_override 逻辑;这个方法就是那个通用逻辑实际跑本地命令的出口。
detect_local_fsmonitor_override427–430 ↗
async fn detect_local_fsmonitor_override(git: &Path, cwd: &Path) -> crate::FsmonitorOverride
作用:判断本地运行 Git 命令时,应该怎样设置 fsmonitor。它的目标是避免不可信的外部 fsmonitor helper 被 Git 自动执行。
数据流:输入 git 程序路径和工作目录 → 包装成 LocalFsmonitorProbeRunner → 调用通用检测逻辑 → 返回一个 fsmonitor 覆盖配置,后续 Git 命令会带上它。
调用关系:get_has_changes 和 diff_against_sha 在会触碰工作区的命令前会调用它。测试也直接调用它,验证 helper 被禁用、内置 fsmonitor 能保留。
调用图:被 4 处调用(diff_against_sha, get_has_changes, fsmonitor_override_rejects_configured_helper, fsmonitor_override_uses_effective_layered_config_value);外部调用 1 个(detect_fsmonitor_override)。
run_git_command_with_timeout_from432–454 ↗
async fn run_git_command_with_timeout_from(
git: &Path,
args: &[&str],
cwd: &Path,
fsmonitor: crate::FsmonitorOverride,
) -> Option<std::process::Output>
作用:这是实际启动 Git 进程的底层函数。它统一加上安全配置:禁用可选锁、禁用 hooks,并按传入的 fsmonitor 策略运行。
数据流:输入 git 路径、参数、工作目录和 fsmonitor 设置 → 构造命令行和环境变量 → 加上 hooksPath 指向空设备、加上 fsmonitor 配置 → 等待最多 5 秒 → 返回输出,超时或启动失败返回空。
调用关系:run_git_command_with_timeout 是它的简化包装。get_has_changes 和 diff_against_sha 会先检测 fsmonitor,再直接调用它。相关测试也用它检查最终 Git 参数是否正确。
调用图:调用 1 个内部函数(git_config_arg);被 5 处调用(diff_against_sha, get_has_changes, run_git_command_with_timeout, fsmonitor_override_rejects_configured_helper, fsmonitor_override_uses_effective_layered_config_value);外部调用 3 个(new, format!, timeout)。
get_git_remotes456–471 ↗
async fn get_git_remotes(cwd: &Path) -> Option<Vec<String>>
作用:读取仓库里配置的远端名称列表,比如 origin、upstream。它还会把 origin 放到最前面,因为 origin 通常是最主要的远端。
数据流:输入工作目录 → 执行 git remote → 把输出按行转成远端名列表 → 如果有 origin,就移到列表第一位 → 返回列表,失败则返回空。
调用关系:git_diff_to_remote、get_default_branch 和 branch_ancestry 都需要远端列表来判断默认分支或远端基准提交,因此会调用它。
调用图:调用 1 个内部函数(run_git_command_with_timeout);被 3 处调用(branch_ancestry, get_default_branch, git_diff_to_remote);外部调用 1 个(from_utf8)。
get_default_branch479–522 ↗
async fn get_default_branch(cwd: &Path) -> Option<String>
作用:推断仓库的默认分支名,比如 main 或 master。默认分支就是通常作为项目主线的分支。
数据流:输入工作目录 → 先读取远端列表 → 对每个远端优先看 refs/remotes/<remote>/HEAD 指向哪里 → 不行再解析 git remote show 的 HEAD branch 行 → 还不行就看本地是否有 main 或 master → 返回找到的名字。
调用关系:branch_ancestry 和 default_branch_name 会调用它。它内部会用 get_git_remotes、run_git_command_with_timeout,并在远端信息不足时交给 get_default_branch_local。
调用图:调用 3 个内部函数(get_default_branch_local, get_git_remotes, run_git_command_with_timeout);被 2 处调用(branch_ancestry, default_branch_name);外部调用 2 个(from_utf8, format!)。
default_branch_name530–532 ↗
async fn default_branch_name(cwd: &Path) -> Option<String>
作用:对外提供一个简单入口,用来获取当前仓库默认分支名。调用者不需要知道内部是查远端还是查本地。
数据流:输入工作目录 → 直接调用 get_default_branch → 把推断出的分支名或空结果返回。
调用关系:它是 get_default_branch 的公开包装函数,适合其他模块只想问“默认分支叫什么”时使用。
调用图:调用 1 个内部函数(get_default_branch)。
get_default_branch_local535–554 ↗
async fn get_default_branch_local(cwd: &Path) -> Option<String>
作用:只从本地分支里猜默认分支。它按惯例检查 main 和 master 是否存在。
数据流:输入工作目录 → 依次执行 Git 命令验证 refs/heads/main 和 refs/heads/master → 哪个先存在就返回哪个 → 都不存在则返回空。
调用关系:get_default_branch 在远端信息不可用时会调用它兜底。local_git_branches 也会用它把默认分支排到列表前面。
调用图:调用 1 个内部函数(run_git_command_with_timeout);被 2 处调用(get_default_branch, local_git_branches);外部调用 1 个(format!)。
branch_ancestry558–623 ↗
async fn branch_ancestry(cwd: &Path) -> Option<Vec<String>>
作用:整理一组可能作为当前工作基准的分支名。它从当前分支、默认分支、以及包含当前 HEAD 的远端分支里挑候选项。
数据流:输入工作目录 → 读取当前分支,排除 detached HEAD 状态 → 读取默认分支 → 放入候选列表并去重 → 再查看哪些远端分支包含当前 HEAD → 继续加入候选 → 返回候选分支列表。
调用关系:git_diff_to_remote 会调用它来准备候选分支。它内部依赖 get_default_branch、get_git_remotes 和多次 run_git_command_with_timeout。
调用图:调用 3 个内部函数(get_default_branch, get_git_remotes, run_git_command_with_timeout);被 1 处调用(git_diff_to_remote);外部调用 4 个(new, from_utf8, new, format!)。
branch_remote_and_distance629–705 ↗
async fn branch_remote_and_distance(
cwd: &Path,
branch: &str,
remotes: &[String],
) -> Option<(Option<GitSha>, usize)>
作用:针对一个分支,查它在远端有没有对应提交,并计算当前 HEAD 比它领先多少个提交。这个距离用来判断哪个远端基准离当前代码最近。
数据流:输入工作目录、分支名和远端列表 → 按远端顺序找 refs/remotes/<remote>/<branch> 是否存在 → 找到就记下远端提交号 → 再用 rev-list --count 计算 branch..HEAD 或 remote_ref..HEAD 的提交数 → 返回远端提交号和距离。
调用关系:find_closest_sha 会对每个候选分支调用它。它所有 Git 查询都通过 run_git_command_with_timeout 完成。
调用图:调用 2 个内部函数(run_git_command_with_timeout, new);被 1 处调用(find_closest_sha);外部调用 2 个(from_utf8, format!)。
find_closest_sha708–730 ↗
async fn find_closest_sha(cwd: &Path, branches: &[String], remotes: &[String]) -> Option<GitSha>
作用:从一组候选分支里选出离当前 HEAD 最近、并且确实存在于远端的提交号。这个提交会作为生成 diff 的基准点。
数据流:输入工作目录、候选分支和远端列表 → 逐个分支调用 branch_remote_and_distance → 跳过没有远端提交的分支 → 保留距离最小的提交号 → 返回最佳 GitSha。
调用关系:git_diff_to_remote 在拿到候选分支后调用它。它把每个分支的细查工作交给 branch_remote_and_distance。
调用图:调用 1 个内部函数(branch_remote_and_distance);被 1 处调用(git_diff_to_remote)。
diff_against_sha732–796 ↗
async fn diff_against_sha(cwd: &Path, sha: &GitSha) -> Option<String>
作用:生成当前工作目录相对某个提交号的差异文本,还会把未被 Git 跟踪的新文件也补进去。这样得到的 diff 更完整。
数据流:输入工作目录和基准提交号 → 检测 fsmonitor 配置 → 执行 git diff 得到已跟踪文件的改动 → 再列出未跟踪文件 → 对每个新文件用 git diff --no-index 和空设备比较 → 把所有差异拼接成一个字符串返回。
调用关系:git_diff_to_remote 找到远端基准提交后会调用它。它使用 detect_local_fsmonitor_override 和 run_git_command_with_timeout_from,并用 join_all 并行处理多个未跟踪文件。
调用图:调用 2 个内部函数(detect_local_fsmonitor_override, run_git_command_with_timeout_from);被 1 处调用(git_diff_to_remote);外部调用 4 个(new, from_utf8, cfg!, join_all)。
resolve_root_git_project_for_trust802–835 ↗
async fn resolve_root_git_project_for_trust(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
) -> Option<AbsolutePathBuf>
作用:找出做“项目信任检查”时应该使用的真正 Git 项目根目录。普通仓库直接用当前仓库根;worktree 则会回到主仓库根。
数据流:输入文件系统接口和当前路径 → 先找当前工作区的 Git 根 → 检查 .git 是目录还是文件 → 如果是目录就返回当前根 → 如果是文件,就读取里面的 gitdir 指向 → 识别 worktrees 结构 → 推回主仓库根目录返回。
调用关系:它先调用 get_git_repo_root_with_fs,再通过文件系统接口读取 .git 元信息。它不用 git 命令,因此也能在远程或受限文件系统里工作。
调用图:调用 4 个内部函数(read_file_text, get_git_repo_root_with_fs, resolve_path_against_base, from_abs_path);外部调用 2 个(new, get_metadata)。
find_ancestor_git_entry837–854 ↗
fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)>
作用:从某个本地目录开始,一直往父目录找 .git。它是本地路径判断 Git 仓库位置的基础小工具。
数据流:输入起始目录 → 检查当前目录下是否有 .git → 没有就退到上一级 → 重复直到文件系统根目录 → 找到就返回仓库根和 .git 路径,否则返回空。
调用关系:get_git_repo_root 会调用它完成真正的向上查找。它只看本机文件系统,不调用 Git。
调用图:被 1 处调用(get_git_repo_root);外部调用 1 个(to_path_buf)。
find_ancestor_git_entry_with_fs856–872 ↗
async fn find_ancestor_git_entry_with_fs(
fs: &dyn ExecutorFileSystem,
base_dir: &AbsolutePathBuf,
) -> Option<(AbsolutePathBuf, AbsolutePathBuf)>
作用:用抽象文件系统向上找 .git,适合远程环境或虚拟文件系统。它和 find_ancestor_git_entry 做同一件事,只是查文件的方式不同。
数据流:输入文件系统接口和起始绝对路径 → 遍历这个路径的所有父级目录 → 每层把 .git 转成 URI 后询问文件系统是否存在 → 找到就返回仓库根和 .git 路径。
调用关系:get_git_repo_root_with_fs 会调用它。它通过 ExecutorFileSystem 查询文件,不直接访问本机磁盘。
调用图:调用 2 个内部函数(ancestors, from_abs_path);被 1 处调用(get_git_repo_root_with_fs);外部调用 1 个(get_metadata)。
local_git_branches876–900 ↗
async fn local_git_branches(cwd: &Path) -> Vec<String>
作用:列出本地 Git 分支,并尽量把默认分支放在最前面。这样界面展示分支时更符合用户习惯。
数据流:输入工作目录 → 执行 git branch --format 获取短分支名 → 清理空行并排序 → 查询本地默认分支 main/master → 如果它在列表中就移到第一位 → 返回分支名列表。
调用关系:它用 run_git_command_with_timeout 读取分支列表,并调用 get_default_branch_local 决定是否需要把默认分支提前。
调用图:调用 2 个内部函数(get_default_branch_local, run_git_command_with_timeout);外部调用 2 个(from_utf8_lossy, new)。
current_branch_name903–912 ↗
async fn current_branch_name(cwd: &Path) -> Option<String>
作用:读取当前检出的分支名。它用于需要显示或记录“现在在哪个分支上”的场景。
数据流:输入工作目录 → 执行 git branch --show-current → 命令成功后把输出转成字符串并去掉空白 → 非空就返回分支名,否则返回空。
调用关系:它通过 run_git_command_with_timeout 执行 Git 命令,是一个独立的轻量查询函数。
调用图:调用 1 个内部函数(run_git_command_with_timeout);外部调用 1 个(from_utf8)。
tests::canonicalize_git_remote_url_normalizes_github_variants922–937 ↗
fn canonicalize_git_remote_url_normalizes_github_variants()
作用:测试各种 GitHub 远端地址写法都会被整理成同一个标准结果。它保证地址比较不会因为 ssh、https、大小写或 .git 后缀而误判。
数据流:输入一组不同格式的 GitHub 地址样例 → 分别调用规范化逻辑 → 用断言检查结果都等于 github.com/openai/codex。
调用关系:这是 canonicalize_git_remote_url 相关的单元测试,专门覆盖 GitHub 常见写法。
调用图:外部调用 1 个(assert_eq!)。
tests::canonicalize_git_remote_url_handles_ghe_without_lowercasing_path940–949 ↗
fn canonicalize_git_remote_url_handles_ghe_without_lowercasing_path()
作用:测试 GitHub Enterprise 或公司自建 Git 服务的路径大小写不会被随便改掉。因为有些自建服务可能区分路径大小写。
数据流:输入两个自建 Git 服务地址 → 调用规范化逻辑 → 检查主机被统一,但 Org/Repo 这样的路径大小写仍保留。
调用关系:它验证 canonicalize_git_remote_url 和底层主机路径规范化逻辑在非 github.com 主机上的特殊行为。
调用图:外部调用 1 个(assert_eq!)。
tests::canonicalize_git_remote_url_rejects_non_repository_values952–956 ↗
fn canonicalize_git_remote_url_rejects_non_repository_values()
作用:测试一些不像远端仓库地址的字符串会被拒绝。这样程序不会把普通路径或不完整地址误当成仓库。
数据流:输入空字符串、本地 file 地址、不完整 GitHub 地址和本地路径 → 调用规范化逻辑 → 检查结果都是空。
调用关系:它保护 canonicalize_git_remote_url 的边界行为,确保无效输入不会产生看似正常的仓库标识。
调用图:外部调用 1 个(assert_eq!)。
tests::fsmonitor_override_rejects_configured_helper960–1011 ↗
async fn fsmonitor_override_rejects_configured_helper()
作用:测试当仓库配置了外部 fsmonitor helper 时,运行工作区相关 Git 命令会把它禁用。这样可以避免执行项目或用户配置里的额外程序。
数据流:创建临时目录和假的 git 脚本 → 假 git 在查询配置时返回一个 helper 路径 → 调用 fsmonitor 检测和 Git 命令运行函数 → 检查最终命令带上 core.fsmonitor=false,输出也符合预期。
调用关系:它直接调用 detect_local_fsmonitor_override 和 run_git_command_with_timeout_from,验证这两个函数配合时会拒绝不安全 helper。
调用图:调用 2 个内部函数(detect_local_fsmonitor_override, run_git_command_with_timeout_from);外部调用 6 个(assert_eq!, format!, metadata, set_permissions, write, tempdir)。
tests::fsmonitor_override_uses_effective_layered_config_value1015–1098 ↗
async fn fsmonitor_override_uses_effective_layered_config_value()
作用:测试当全局配置有外部 helper、但仓库本地配置启用了 Git 内置 fsmonitor 时,程序会采用最终生效的安全配置。它避免把安全的内置加速错误关掉。
数据流:创建临时 Git 仓库、假的 git 包装脚本、全局配置和本地配置 → 全局写 helper,本地写 core.fsmonitor=true → 运行检测和状态命令 → 检查最终命令使用 core.fsmonitor=true,并且查询顺序符合预期。
调用关系:它同样覆盖 detect_local_fsmonitor_override 和 run_git_command_with_timeout_from,但重点验证分层 Git 配置下的最终有效值处理。
调用图:调用 2 个内部函数(detect_local_fsmonitor_override, run_git_command_with_timeout_from);外部调用 9 个(assert_eq!, new, format!, create_dir, metadata, read_to_string, set_permissions, write, tempdir)。
tui/src/get_git_diff.rs源码 ↗
用户点 /diff 时,程序需要安全、完整地拿到当前工作目录的改动。这个文件先确认当前位置是不是 Git 仓库;不是的话就安静返回“不是仓库”和空内容。是仓库的话,它会探测 Git 的 fsmonitor(文件变化监视器,用来加速 Git 判断文件是否变化),再运行 Git 命令拿已跟踪文件的 diff,并列出未跟踪文件,逐个把它们和空文件比较,拼成一份完整 diff。这里很重要的一点是“安全”:它会关掉 Git hook(Git 自动脚本)和可执行的 filter(Git 配置里的外部处理程序),避免只是看 diff 却意外运行仓库里配置的脚本。它还知道 Git diff 发现差异时退出码可能是 1,这不是错误。
WorkspaceFsmonitorProbeRunner::run_probe35–42 ↗
async fn run_probe(&mut self, args: &[&str]) -> Option<Vec<u8>>
作用:这是给 fsmonitor 探测用的小适配器。它把探测逻辑想运行的 Git 参数,转成当前工作区能执行的命令。
数据流:进去的是一组 Git 参数,以及它自己保存的命令执行器和当前目录 → 它组装成 git ... 命令,在指定目录运行 → 如果命令成功,就把标准输出变成字节返回;失败就返回空,表示这次探测没有可用结果。
调用关系:它是在 get_git_diff 里创建的,交给 detect_fsmonitor_override 使用。探测器负责决定 fsmonitor 怎么处理,而这个函数只负责真正把探测命令跑起来。
调用图:调用 1 个内部函数(new);外部调用 2 个(to_path_buf, run)。
get_git_diff49–120 ↗
async fn get_git_diff(
runner: &dyn WorkspaceCommandExecutor,
cwd: &Path,
) -> Result<(bool, String), String>
作用:这是本文件的主入口,用来拿当前目录的完整 Git diff。调用者给它一个能执行工作区命令的对象和目录,它返回“是不是 Git 仓库”和 diff 文本。
数据流:进去的是命令执行器和当前目录 → 它先检查是否在 Git 仓库里;再探测 fsmonitor;再找出需要临时禁用的 Git filter;然后并行获取已跟踪文件 diff 和未跟踪文件列表;最后把每个未跟踪文件也做成 diff → 出来的是 (true, diff文本),如果不是仓库则是 (false, 空字符串),遇到异常 Git 状态会返回错误文字。
调用关系:它是 /diff 工作流会调用的核心函数。它把活儿分给 inside_git_repo、diff_filter_config_overrides、run_git_capture_diff 和 run_git_capture_stdout,自己负责把这些结果串起来。测试里的多个用例都围绕它验证各种边界情况。
调用图:调用 3 个内部函数(diff_filter_config_overrides, inside_git_repo, run_git_capture_diff);被 7 处调用(get_git_diff_accepts_diff_exit_code_one, get_git_diff_disables_helpers_for_tracked_and_untracked_diffs, get_git_diff_does_not_execute_configured_filters_fsmonitor_or_hooks, get_git_diff_does_not_execute_helpers_while_checking_dirty_submodules, get_git_diff_preserves_builtin_fsmonitor_for_diff_workflow, get_git_diff_rejects_unexpected_git_diff_status, get_git_diff_returns_not_git_for_non_git_cwd);外部调用 6 个(new, new, cfg!, detect_fsmonitor_override, format!, join!)。
run_git_capture_stdout124–139 ↗
async fn run_git_capture_stdout(
runner: &dyn WorkspaceCommandExecutor,
cwd: &Path,
fsmonitor: FsmonitorOverride,
args: &[&str],
) -> Result<String, String>
作用:这个小函数用来运行一个普通 Git 命令,并拿回它的标准输出。这里的“普通”意思是:退出码必须是成功,不能像 diff 那样把 1 当正常。
数据流:进去的是执行器、目录、fsmonitor 设置和 Git 参数 → 它调用统一的 run_git_command 跑命令 → 如果退出码表示成功,就返回输出文本;否则返回一条带命令和退出码的错误信息。
调用关系:它依赖 run_git_command 做真正的命令组装和执行。在 diff 流程里,它适合用来跑 ls-files 这类只要失败就应当报错的 Git 查询命令。
调用图:调用 1 个内部函数(run_git_command);外部调用 1 个(format!)。
run_git_capture_diff143–159 ↗
async fn run_git_capture_diff(
runner: &dyn WorkspaceCommandExecutor,
cwd: &Path,
fsmonitor: FsmonitorOverride,
config_overrides: &[(String, String)],
args: &[&str],
) -> Result<St
作用:这个函数专门跑 Git diff 类命令。它知道 Git 的一个特点:发现文件有差异时,退出码可能是 1,但这并不代表出错。
数据流:进去的是执行器、目录、fsmonitor 设置、临时 Git 配置覆盖项和 Git diff 参数 → 它通过 run_git_command 执行 → 如果退出码是 0 或 1,就把 diff 文本返回;其他退出码才变成错误。
调用关系:get_git_diff 用它获取已跟踪文件的 diff,也用它为每个未跟踪文件生成 diff。它把 Git diff 的特殊退出码规则包起来,避免上层误判。
调用图:调用 1 个内部函数(run_git_command);被 1 处调用(get_git_diff);外部调用 1 个(format!)。
diff_filter_config_overrides163–205 ↗
async fn diff_filter_config_overrides(
runner: &dyn WorkspaceCommandExecutor,
cwd: &Path,
fsmonitor: FsmonitorOverride,
) -> Result<Vec<(String, String)>, String>
作用:这个函数找出仓库里配置过的可执行 Git filter,并生成一组临时配置,让这些外部程序在生成 diff 时不要被运行。
数据流:进去的是执行器、目录和 fsmonitor 设置 → 它运行 git config 查找类似 filter.xxx.clean 和 filter.xxx.process 的配置 → 去重后为每个 filter 生成空的 clean/process 配置,并把 required 设成 false → 出来的是一组要传给 Git 的临时配置覆盖项。
调用关系:get_git_diff 在真正跑 diff 前调用它。之后 run_git_command 会把这些覆盖项放进环境变量里,让 Git 本次执行时临时忽略那些危险或多余的外部 filter。
调用图:调用 1 个内部函数(run_git_command);被 1 处调用(get_git_diff);外部调用 1 个(format!)。
inside_git_repo208–223 ↗
async fn inside_git_repo(
runner: &dyn WorkspaceCommandExecutor,
cwd: &Path,
) -> Result<bool, String>
作用:这个函数判断当前目录是不是在 Git 工作区里。这样 /diff 在普通文件夹里不会硬跑一堆 Git 命令。
数据流:进去的是执行器和目录 → 它运行 git rev-parse --is-inside-work-tree,并且明确禁用 fsmonitor → 如果命令成功就返回 true,否则返回 false。
调用关系:get_git_diff 一开始就调用它。只有它确认是 Git 仓库后,后面的 fsmonitor 探测、filter 检查和 diff 生成才会继续。
调用图:调用 1 个内部函数(run_git_command);被 1 处调用(get_git_diff)。
run_git_command225–254 ↗
async fn run_git_command(
runner: &dyn WorkspaceCommandExecutor,
cwd: &Path,
fsmonitor: FsmonitorOverride,
config_overrides: &[(String, String)],
args: &[&str],
) -> Result<Workspa
作用:这是所有 Git 命令的统一出口。它负责把安全开关、超时、当前目录和临时配置都装到命令里,再交给工作区执行器运行。
数据流:进去的是执行器、目录、fsmonitor 设置、临时配置覆盖项和 Git 参数 → 它拼出 git -c ... 命令,设置 30 秒超时,关闭输出大小限制,并把配置覆盖项写入环境变量 → 出来的是命令输出;如果执行器本身报错,就转成字符串错误。
调用关系:inside_git_repo、diff_filter_config_overrides、run_git_capture_stdout 和 run_git_capture_diff 都通过它跑 Git。它像统一的安全闸门,保证这些 Git 调用使用同一套防护设置。
调用图:调用 2 个内部函数(git_config_arg, new);被 4 处调用(diff_filter_config_overrides, inside_git_repo, run_git_capture_diff, run_git_capture_stdout);外部调用 3 个(to_path_buf, format!, run)。
tests::get_git_diff_returns_not_git_for_non_git_cwd275–290 ↗
async fn get_git_diff_returns_not_git_for_non_git_cwd()
作用:这个测试确认:当前目录不是 Git 仓库时,主函数不会报错,也不会伪造 diff。
数据流:进去的是一个假执行器,预设 rev-parse 返回失败 → 测试调用 get_git_diff → 期望结果是 (false, 空字符串),并检查命令的目录、超时等元信息符合要求。
调用关系:它直接测试 get_git_diff 的第一道分支,也借助 FakeRunner 模拟 Git 返回结果,最后用 assert_command_metadata 检查命令包装是否正确。
调用图:调用 1 个内部函数(get_git_diff);外部调用 5 个(from, assert_eq!, new, assert_command_metadata, vec!)。
tests::get_git_diff_disables_helpers_for_tracked_and_untracked_diffs293–387 ↗
async fn get_git_diff_disables_helpers_for_tracked_and_untracked_diffs()
作用:这个测试确认:生成已跟踪和未跟踪文件 diff 时,会禁用仓库配置的外部 filter helper,避免看 diff 时执行外部程序。
数据流:进去的是一串假 Git 响应:仓库存在、fsmonitor 探测、发现 filter、返回 tracked diff、列出新文件、返回新文件 diff → 调用 get_git_diff → 期望拼出 tracked\nuntracked\n,并检查相关 diff 命令带上了禁用 filter 的环境变量。
调用关系:它覆盖 get_git_diff、diff_filter_config_overrides 和 run_git_capture_diff 的配合。filter_override_env 用来生成测试期望的环境变量。
调用图:调用 1 个内部函数(get_git_diff);外部调用 5 个(from, assert_eq!, new, assert_command_metadata, vec!)。
tests::get_git_diff_preserves_builtin_fsmonitor_for_diff_workflow390–473 ↗
async fn get_git_diff_preserves_builtin_fsmonitor_for_diff_workflow()
作用:这个测试确认:如果 Git 使用的是内置 fsmonitor,diff 流程会保留它,而不是一刀切禁用。
数据流:进去的是假执行器响应:仓库存在、配置显示 fsmonitor 为 true、Git 版本支持内置 daemon、然后正常返回 diff 和未跟踪文件 → 调用 get_git_diff → 期望得到 tracked 和 untracked 拼接后的 diff,并且命令元信息正确。
调用关系:它验证 WorkspaceFsmonitorProbeRunner::run_probe 和外部的 detect_fsmonitor_override 探测结果,会被 get_git_diff 传递给后续 Git 命令。
调用图:调用 1 个内部函数(get_git_diff);外部调用 5 个(from, assert_eq!, new, assert_command_metadata, vec!)。
tests::get_git_diff_accepts_diff_exit_code_one476–535 ↗
async fn get_git_diff_accepts_diff_exit_code_one()
作用:这个测试确认:Git diff 返回退出码 1 时,只要这是因为有差异,就应该被当成正常结果。
数据流:进去的是假 Git 响应,其中 diff 命令退出码为 1 且输出 tracked\n,未跟踪文件列表为空 → 调用 get_git_diff → 期望成功返回 (true, "tracked\n")。
调用关系:它主要盯住 run_git_capture_diff 的特殊规则,确保 get_git_diff 不会把“发现差异”误报成失败。
调用图:调用 1 个内部函数(get_git_diff);外部调用 5 个(from, assert_eq!, new, assert_command_metadata, vec!)。
tests::get_git_diff_rejects_unexpected_git_diff_status538–602 ↗
async fn get_git_diff_rejects_unexpected_git_diff_status()
作用:这个测试确认:Git diff 返回真正异常的退出码时,主函数会把错误交给调用者,而不是吞掉。
数据流:进去的是假 Git 响应,其中 diff 命令退出码为 2 → 调用 get_git_diff → 期望得到一条明确错误,说明是哪条 Git 命令以什么状态失败。
调用关系:它测试 run_git_capture_diff 对退出码的边界判断,并证明错误会一路传回 get_git_diff 的调用者。
调用图:调用 1 个内部函数(get_git_diff);外部调用 5 个(from, assert_eq!, new, assert_command_metadata, vec!)。
tests::get_git_diff_does_not_execute_configured_filters_fsmonitor_or_hooks606–683 ↗
async fn get_git_diff_does_not_execute_configured_filters_fsmonitor_or_hooks()
作用:这个 Unix 测试用真实 Git 仓库确认安全防护真的生效:filter、fsmonitor helper 和 hook 都不会被执行。
数据流:进去的是临时目录里的真实仓库,里面故意配置会写标记文件的 filter、fsmonitor helper 和 hook → 调用 get_git_diff → 期望 diff 里能看到修改前后内容,但那些标记文件都没有出现,说明外部脚本没跑。
调用关系:它比假执行器测试更接近真实环境,验证 run_git_command 的禁用 hook、禁用危险 filter、fsmonitor 覆盖策略在真实 Git 下有效。
调用图:调用 1 个内部函数(get_git_diff);外部调用 8 个(from_secs, assert_eq!, create_dir, write, sleep, tempdir, run_git_setup, write_marker_helper)。
tests::get_git_diff_does_not_execute_helpers_while_checking_dirty_submodules687–742 ↗
async fn get_git_diff_does_not_execute_helpers_while_checking_dirty_submodules()
作用:这个 Unix 测试确认:检查含子模块的仓库时,不会深入子模块工作区去触发里面配置的外部 helper。
数据流:进去的是一个父仓库和一个子模块,子模块里配置了会写标记文件的 filter helper → 调用父仓库上的 get_git_diff → 期望 diff 为空,并且 helper 的标记文件不存在。
调用关系:它验证 get_git_diff 使用的 diff 参数,特别是忽略脏子模块工作区的设置,能避免对子模块里的危险配置产生副作用。
调用图:调用 1 个内部函数(get_git_diff);外部调用 8 个(from_secs, assert_eq!, create_dir, write, sleep, tempdir, run_git_setup, write_marker_helper)。
tests::git_command744–756 ↗
fn git_command(fsmonitor: FsmonitorOverride, args: &[&str]) -> Vec<String>
作用:这个测试辅助函数生成测试里期望看到的完整 Git 命令参数列表。
数据流:进去的是 fsmonitor 设置和 Git 子命令参数 → 它补上 git、fsmonitor 的 -c 配置、禁用 hook 的 -c 配置 → 出来是一组字符串,供假执行器核对实际命令是否一致。
调用关系:多个测试用它构造 FakeRunner 的预期命令。它让测试和生产代码对命令格式的要求保持同步。
调用图:调用 1 个内部函数(git_config_arg)。
tests::git_probe_command758–764 ↗
fn git_probe_command(args: &[&str]) -> Vec<String>
作用:这个测试辅助函数生成 fsmonitor 探测阶段期望执行的 Git 命令。
数据流:进去的是探测用 Git 参数 → 它只在前面加上 git,不额外加生产 diff 命令的安全配置 → 出来是一组字符串参数。
调用关系:它用于测试 WorkspaceFsmonitorProbeRunner::run_probe 交给 Git 的探测命令,和普通 diff 命令区分开。
tests::filter_override_env766–785 ↗
fn filter_override_env(driver: &str) -> HashMap<String, Option<String>>
作用:这个测试辅助函数生成“禁用某个 Git filter”时应该出现的环境变量。
数据流:进去的是 filter driver 名字,比如 filter.evil → 它生成 GIT_CONFIG_COUNT 以及对应的 key/value 环境变量,把 clean 和 process 设空,把 required 设为 false → 出来是一张环境变量表。
调用关系:相关测试用它和实际命令的环境变量做比较,验证 diff_filter_config_overrides 与 run_git_command 的配合没有出错。
tests::response787–796 ↗
fn response(argv: Vec<String>, exit_code: i32, stdout: &str) -> FakeResponse
作用:这个测试辅助函数把一条预期命令和一份假 Git 输出打包成假响应。
数据流:进去的是预期参数列表、退出码和标准输出文本 → 它创建 FakeResponse,里面包含命令和 WorkspaceCommandOutput → 出来给 FakeRunner 按顺序消费。
调用关系:所有使用 FakeRunner 的测试都靠它搭建 Git 的假返回,方便精确控制每一步发生什么。
调用图:外部调用 1 个(new)。
tests::null_device798–800 ↗
fn null_device() -> &'static str
作用:这个测试辅助函数返回当前系统的空设备路径。空设备可以理解成一个永远为空的文件。
数据流:进去没有参数 → 它根据系统判断 Windows 用 NUL,其他系统用 /dev/null → 出来是对应字符串。
调用关系:未跟踪文件 diff 会把新文件和空设备比较。测试用它生成和生产代码一致的期望命令。
调用图:外部调用 1 个(cfg!)。
tests::run_git_setup803–816 ↗
fn run_git_setup(cwd: &Path, args: &[&str])
作用:这个 Unix 测试辅助函数在真实临时仓库里运行 Git 初始化和配置命令。
数据流:进去的是工作目录和 Git 参数 → 它启动真实 git 进程并等待输出 → 如果退出码不是 0,测试立刻失败,并打印标准输出和错误输出帮助排查。
调用关系:真实仓库测试用它搭建仓库、提交文件、配置 filter、添加子模块等。它只用于测试准备阶段。
调用图:外部调用 2 个(new, assert_eq!)。
tests::write_marker_helper819–827 ↗
fn write_marker_helper(path: &Path)
作用:这个 Unix 测试辅助函数写一个会留下“我被执行过”标记的小脚本。
数据流:进去的是脚本路径 → 它写入 shell 脚本内容,脚本运行时会生成 .ran 标记文件,然后把脚本设为可执行 → 出来没有返回值,但磁盘上多了这个可执行测试脚本。
调用关系:真实安全测试用它制造 filter、fsmonitor 和 hook helper。之后测试通过检查 .ran 文件是否存在,判断这些 helper 有没有被意外执行。
调用图:外部调用 3 个(metadata, set_permissions, write)。
tests::assert_command_metadata829–845 ↗
fn assert_command_metadata(commands: &[WorkspaceCommand], cwd: &Path)
作用:这个测试辅助函数检查所有捕获到的工作区命令是否带了正确的目录、超时和输出限制。
数据流:进去的是已记录命令列表和期望工作目录 → 它逐条检查 cwd;对 fsmonitor 探测类命令要求短超时和输出上限;对 diff 类命令要求 30 秒超时并关闭输出上限 → 不返回值,检查失败就让测试失败。
调用关系:多个假执行器测试在调用 get_git_diff 后使用它,确保命令不只是参数对,运行保护条件也对。
调用图:外部调用 2 个(assert_eq!, matches!)。
tests::FakeRunner::new858–863 ↗
tests::FakeRunner::commands865–872 ↗
fn commands(&self) -> Vec<WorkspaceCommand>
作用:这个函数取出假执行器记录过的所有命令,并顺便确认预设响应都用完了。
数据流:进去没有额外参数,只读取 FakeRunner 内部状态 → 它先检查响应队列为空,再复制已记录命令列表 → 出来是一组命令,供测试继续断言。
调用关系:测试在 get_git_diff 跑完后调用它,检查生产代码到底执行了哪些命令,以及有没有漏跑或多跑。
调用图:外部调用 1 个(assert_eq!)。
tests::FakeRunner::run876–893 ↗
fn run(
&self,
command: WorkspaceCommand,
) -> Pin<
Box<
dyn Future<Output = Result<WorkspaceCommandOutput, WorkspaceCommandError>>
作用:这是 FakeRunner 对命令执行接口的实现。它不真的运行命令,而是按队列吐出假结果。
数据流:进去的是生产代码要运行的 WorkspaceCommand → 它取出下一条预设响应,先确认命令参数完全匹配,再把命令记下来 → 出来是假响应里的 WorkspaceCommandOutput。
调用关系:get_git_diff 以为自己在调用正常的 WorkspaceCommandExecutor。测试借这个实现精确验证每条 Git 命令,同时避免依赖真实系统环境。
调用图:外部调用 2 个(pin, assert_eq!)。
tests::LocalRunner::run901–933 ↗
fn run(
&self,
command: WorkspaceCommand,
) -> Pin<
Box<
dyn Future<Output = Result<WorkspaceCommandOutput, WorkspaceCommandError>>
作用:这是 Unix 真实环境测试用的命令执行器。它会真的启动系统里的 Git 命令。
数据流:进去的是 WorkspaceCommand,里面有程序名、参数、目录和环境变量 → 它创建系统进程,设置当前目录和环境变量,执行并收集输出 → 出来是 WorkspaceCommandOutput,包含退出码、标准输出和标准错误。
调用关系:真实仓库安全测试用它调用 get_git_diff。和 FakeRunner 不同,它验证代码在真实 Git 行为下也不会触发 filter、fsmonitor helper 或 hook。
插件打包和市场更新
这些文件涵盖插件归档传输,以及用于安全安装和启用市场内容的文件系统与 git 辅助工具。
core-plugins/src/plugin_bundle_archive.rs源码 ↗
可以把这个文件想成“插件行李打包和开箱检查员”。打包时,它先确认给的是一个真正的插件目录,并且里面有 .codex-plugin/plugin.json 这个插件身份证;然后按固定顺序把目录里的普通文件和文件夹放进 tar 包,再用 gzip 压缩,同时用一个带上限的内存缓冲区防止包太大。解包时,它先建好目标目录,再逐个检查压缩包里的条目:只能是普通文件或文件夹,不能是符号链接、硬链接这类可能绕路的东西;路径也不能包含“回到上一级”这种会逃出目标目录的写法;所有解出来的文件总大小还要受限制。这样既能方便传插件,又能避免压缩包炸弹、路径穿越等常见安全坑。
PluginBundleUnpackError::io47–49 ↗
fn io(context: &'static str, source: io::Error) -> Self
作用:这个小函数把底层读写错误包装成“解插件包失败”的错误,并顺手加上一句说明发生在什么步骤。这样上层看到错误时,不只是知道失败了,还知道大概卡在哪里。
数据流:进去的是一段固定的上下文说明和一个系统读写错误 → 它把两者装进 PluginBundleUnpackError::Io 这个错误类型里 → 出来的是一个更适合给插件解包流程使用的错误对象,不改动文件或目录。
调用关系:它是解包流程里的错误翻译器。unpack_plugin_bundle_tar_gz 和 unpack_plugin_bundle_tar 在创建目录、读取 tar 包、解文件失败时都会用同样的方式构造这类错误,让错误信息保持一致。
pack_plugin_bundle_tar_gz52–77 ↗
fn pack_plugin_bundle_tar_gz(
plugin_path: &Path,
max_bytes: usize,
) -> Result<Vec<u8>, PluginBundlePackError>
作用:这个函数把一个插件文件夹打成 tar.gz 压缩包,并返回一段可以上传或保存的字节。它还会检查插件目录是否合法,以及压缩后的包有没有超过允许大小。
数据流:进去的是插件目录路径和最大压缩包大小 → 它先确认路径是目录,再确认里面有 .codex-plugin/plugin.json,然后创建 gzip 压缩器和 tar 打包器,把插件目录内容交给 append_plugin_tree 加进去,最后收尾压缩 → 出来的是 Vec<u8> 字节数组;如果路径不对、文件读写失败或包太大,就返回明确的错误。
调用关系:它是打包入口,会被 archive_plugin_for_upload_with_limit 在准备上传插件时调用。它自己负责前置检查和收尾,真正遍历目录的活交给 append_plugin_tree,大小超限等底层错误再通过 archive_io_error 翻译成好懂的打包错误。
调用图:调用 2 个内部函数(new, append_plugin_tree);被 1 处调用(archive_plugin_for_upload_with_limit);外部调用 6 个(new, is_dir, join, to_path_buf, default, new)。
append_plugin_tree79–108 ↗
fn append_plugin_tree(
archive: &mut tar::Builder<W>,
plugin_root: &Path,
current: &Path,
) -> io::Result<()>
作用:这个函数把插件目录里的文件和子目录一个个加入 tar 包。它会按名字排序,所以同一个目录打出来的包更稳定,不会因为系统读取目录顺序不同而变化。
数据流:进去的是正在写的 tar 打包器、插件根目录、当前正在遍历的目录 → 它读取当前目录的所有条目,排序后逐个处理:目录就先加入目录项再递归进入,普通文件就按相对路径加入包里,其他类型直接报错 → 出来是 tar 包被继续填充;遇到无法读取、路径算不出来或不支持的文件类型时返回 io 错误。
调用关系:它只在 pack_plugin_bundle_tar_gz 打包时被调用,是打包过程里的“目录扫描员”。它把文件真正写进 tar 打包器,出现的读写错误会一路传回 pack_plugin_bundle_tar_gz,再由 archive_io_error 转成插件打包错误。
调用图:被 1 处调用(pack_plugin_bundle_tar_gz);外部调用 5 个(append_dir, append_path_with_name, other, format!, read_dir)。
archive_io_error110–122 ↗
fn archive_io_error(source: io::Error) -> PluginBundlePackError
作用:这个函数把普通的输入输出错误翻译成插件打包专用错误。特别是当错误其实表示“压缩包太大”时,它会把这个原因单独拎出来,方便上层给出准确提示。
数据流:进去的是一个 io::Error,也就是底层读写错误 → 它检查这个错误里面是否藏着 ArchiveSizeLimitExceeded 这个“大小超限”标记 → 如果是,就输出 ArchiveTooLarge;如果不是,就输出普通 Io 打包错误,不改动任何外部状态。
调用关系:它服务于 pack_plugin_bundle_tar_gz。打包、压缩、写内存缓冲区时产生的错误都会经过它过滤一遍,这样 SizeLimitedBuffer::write 里制造的大小超限错误能变成更清楚的 PluginBundlePackError::ArchiveTooLarge。
调用图:外部调用 1 个(get_ref)。
unpack_plugin_bundle_tar_gz124–139 ↗
fn unpack_plugin_bundle_tar_gz(
bytes: &[u8],
destination: &Path,
max_total_bytes: u64,
) -> Result<(), PluginBundleUnpackError>
作用:这个函数把 tar.gz 格式的插件包解压到指定目录。它是外部拿到插件包字节后,真正落地成文件夹的入口。
数据流:进去的是压缩包字节、目标目录路径、允许解出的总大小 → 它先创建目标目录,再用 gzip 解压器包住这些字节,然后创建 tar 读取器,最后把安全检查和逐项解包交给 unpack_plugin_bundle_tar → 成功时目标目录里出现插件文件;失败时返回带上下文的解包错误。
调用关系:它是解包 tar.gz 的公开入口,被测试里的 archive_plugin_for_upload_round_trips_through_plugin_bundle_archive_with_long_paths 和 extract_plugin_bundle_tar_gz_with_limits 使用。它只负责准备解压管道,真正逐条检查 tar 内容的是 unpack_plugin_bundle_tar。
调用图:调用 2 个内部函数(unpack_plugin_bundle_tar, new);被 2 处调用(archive_plugin_for_upload_round_trips_through_plugin_bundle_archive_with_long_paths, extract_plugin_bundle_tar_gz_with_limits);外部调用 3 个(new, new, create_dir_all)。
unpack_plugin_bundle_tar141–203 ↗
fn unpack_plugin_bundle_tar(
archive: &mut Archive<R>,
destination: &Path,
max_total_bytes: u64,
) -> Result<(), PluginBundleUnpackError>
作用:这个函数逐个读取 tar 包里的条目,并安全地写到目标目录里。它是防止恶意压缩包乱写文件、塞入链接、或者解出太多内容的核心守门员。
数据流:进去的是 tar 读取器、目标目录、允许解出的总字节数 → 它循环读取每个条目,拿到条目类型、大小和路径,先用 checked_tar_output_path 算出安全输出路径;目录就创建目录,普通文件先用 enforce_total_extracted_size 累计大小再解出来;链接和不支持的类型直接拒绝 → 出来是目标目录被填入合法内容,或返回 InvalidBundle、Io、ExtractedBundleTooLarge 等错误。
调用关系:它由 unpack_plugin_bundle_tar_gz 调用,是解包过程的主工作台。它把路径安全检查交给 checked_tar_output_path,把总大小检查交给 enforce_total_extracted_size,自己负责把这些检查串起来并真正创建目录、解文件。
调用图:调用 2 个内部函数(checked_tar_output_path, enforce_total_extracted_size);被 1 处调用(unpack_plugin_bundle_tar_gz);外部调用 4 个(entries, InvalidBundle, format!, create_dir_all)。
checked_tar_output_path205–234 ↗
fn checked_tar_output_path(
destination: &Path,
entry_name: &Path,
) -> Result<PathBuf, PluginBundleUnpackError>
作用:这个函数检查 tar 包里的路径能不能安全地放进目标目录。它防止压缩包用 ../、绝对路径、Windows 盘符这类写法,把文件偷偷写到目标目录外面。
数据流:进去的是解包目标目录和 tar 条目里的路径名 → 它从路径组件一个个看,只接受普通名字,忽略当前目录符号“.”,拒绝上级目录、根目录和路径前缀;同时要求路径不能为空 → 出来的是拼好的安全输出路径;如果路径有逃逸风险,就返回 InvalidBundle。
调用关系:它被 unpack_plugin_bundle_tar 在处理每个 tar 条目前调用。只有它确认路径安全后,后面的创建目录和写文件才会发生,所以它相当于解包流程里的“地址门禁”。
调用图:被 1 处调用(unpack_plugin_bundle_tar);外部调用 4 个(components, to_path_buf, InvalidBundle, format!)。
enforce_total_extracted_size236–255 ↗
fn enforce_total_extracted_size(
entry_size: u64,
extracted_bytes: &mut u64,
max_total_bytes: u64,
) -> Result<(), PluginBundleUnpackError>
作用:这个函数检查解出来的文件总大小有没有超过上限。它用来防止一个看起来不大的压缩包,解开后变成巨大的内容,把磁盘或内存撑爆。
数据流:进去的是当前文件大小、已经累计解出的大小引用、最大允许总大小 → 它先安全相加,避免数字溢出;再判断新总数是否超过上限 → 如果没超过,就更新累计值并返回成功;如果超过或发生溢出,就返回 ExtractedBundleTooLarge。
调用关系:它由 unpack_plugin_bundle_tar 在每个普通文件解出前调用。它不负责写文件,只负责在写之前做总量把关,确保后续 entry.unpack 不会把过量数据落到磁盘。
调用图:被 1 处调用(unpack_plugin_bundle_tar)。
SizeLimitedBuffer::new263–268 ↗
fn new(max_bytes: usize) -> Self
作用:这个函数创建一个有容量上限的内存缓冲区。打包插件时,压缩结果会先写到这里,用它来保证压缩包不会超过规定大小。
数据流:进去的是最大允许字节数 → 它创建一个空的 Vec<u8> 作为存放压缩包的地方,并记住大小上限 → 出来的是一个 SizeLimitedBuffer 实例,还没有写入任何数据。
调用关系:它被 pack_plugin_bundle_tar_gz 在开始 gzip 压缩前调用。之后 gzip 编码器会把压缩后的字节写进这个缓冲区,而 SizeLimitedBuffer::write 会负责每次写入时检查大小。
调用图:被 1 处调用(pack_plugin_bundle_tar_gz);外部调用 1 个(new)。
SizeLimitedBuffer::into_inner270–272 ↗
fn into_inner(self) -> Vec<u8>
作用:这个函数把 SizeLimitedBuffer 里面已经收集好的字节拿出来。打包完成后,调用者需要的是普通字节数组,而不是带限制功能的外壳。
数据流:进去的是一个已经完成写入的 SizeLimitedBuffer → 它拆掉外层结构,取出内部 Vec<u8> → 出来的是压缩包字节;缓冲区本身被消费掉,不能再继续使用。
调用关系:它在 pack_plugin_bundle_tar_gz 的压缩收尾阶段被使用。gzip 编码器 finish 成功后,打包函数通过它拿到最终要返回给上传流程的字节。
SizeLimitedBuffer::write276–292 ↗
fn write(&mut self, buf: &[u8]) -> io::Result<usize>
作用:这个函数实现“往有上限的缓冲区里写字节”。它像一个带刻度的袋子:每次要塞东西进去前先看会不会超过容量,超过就拒绝。
数据流:进去的是一段要写入的字节 buf,以及缓冲区当前已有的字节和最大上限 → 它计算写完后的长度,先防止数字溢出,再检查是否超过上限;没超过就把字节追加进去并返回写入长度,超过就返回带 ArchiveSizeLimitExceeded 的 io 错误 → 它会改变缓冲区里的 bytes 内容,或者在失败时不追加。
调用关系:它是 Rust 的 Write 写入接口的一部分,会被 gzip 编码器在 pack_plugin_bundle_tar_gz 压缩时自动调用。它产生的大小超限错误之后会被 archive_io_error 识别,并转成 ArchiveTooLarge。
调用图:外部调用 1 个(other)。
SizeLimitedBuffer::flush294–296 ↗
fn flush(&mut self) -> io::Result<()>
作用:这个函数实现写入接口要求的“刷新”动作。因为这个缓冲区只是内存里的 Vec,不像文件或网络那样需要真的刷到外部设备,所以它什么也不用做。
数据流:进去的是当前缓冲区本身 → 它不写入、不清空、不改变任何数据,只直接返回成功 → 出来是 Ok,表示刷新动作完成。
调用关系:它是 Write 接口的配套函数,可能被 gzip 编码器或 tar 写入流程在收尾时调用。它存在是为了让 SizeLimitedBuffer 能像普通输出目标一样被这些库使用。
ArchiveSizeLimitExceeded::fmt306–312 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:这个函数把“压缩包超过大小限制”这个内部错误变成一段人能看懂的文字。这样错误被打印或记录日志时,不会只显示一个冷冰冰的类型名。
数据流:进去的是 ArchiveSizeLimitExceeded 里的实际字节数和最大允许字节数,以及一个格式化输出器 → 它把这些数字写成一句说明:压缩包会有多少字节、超过了多少上限 → 出来是格式化结果,不改变错误对象本身。
调用关系:它是标准 Display 显示接口的一部分。SizeLimitedBuffer::write 在超限时会把 ArchiveSizeLimitExceeded 塞进 io 错误里;如果这个错误需要展示,fmt 就负责生成可读文字,同时 archive_io_error 也能从错误里识别出这个类型。
调用图:外部调用 1 个(write!)。
core-plugins/src/marketplace_add/install.rs源码 ↗
安装 marketplace 不是简单地“下载到一个文件夹”就完事了。这里要处理几个现实问题:名字里可能有危险字符,安装路径可能被伪造成系统外的位置,Git 下载可能失败,还可能只想下载仓库里的某几个子目录。这个文件就像安装工人的工具箱:先把 marketplace 名字变成安全的文件夹名;再确认目标目录确实在允许的安装根目录下面;然后用 Git 把源码克隆到临时暂存目录;最后把暂存目录整体挪到正式位置。这里还支持 Git 的 sparse checkout(稀疏检出,意思是只拿仓库里指定的部分内容,不把整个仓库都下载出来)。run_git 是底层跑 Git 命令的小助手,它会关闭交互式输入,避免程序卡在那里等人输入密码或确认。
clone_git_source7–43 ↗
fn clone_git_source(
url: &str,
ref_name: Option<&str>,
sparse_paths: &[String],
destination: &Path,
) -> Result<(), MarketplaceAddError>
作用:从一个 Git 地址把 marketplace 源码拉到本地目录。它可以拉完整仓库,也可以只拉仓库里的指定路径,还能切到指定分支、标签或提交。
数据流:进去的是 Git 地址、可选的版本名、要稀疏下载的路径列表、以及本地目标目录。它先把目标路径转成 Git 命令能用的字符串;如果不需要稀疏下载,就直接执行 git clone,然后按需要执行 git checkout。如果需要稀疏下载,它会先用不检出文件的方式克隆,再设置要保留的路径,最后检出指定版本或默认的 HEAD。出来的结果是成功安装到目标目录,或者返回一个说明 Git 失败原因的错误。
调用关系:它是实际“下载源码”的核心步骤,会把具体 Git 操作交给 run_git。上层安装流程准备好目标目录和参数后会调用它;它自己不负责检查目录是否安全,这些事情由同文件里的路径检查函数和上层流程先做。
调用图:调用 1 个内部函数(run_git);外部调用 3 个(new, to_string_lossy, vec!)。
safe_marketplace_dir_name45–65 ↗
fn safe_marketplace_dir_name(
marketplace_name: &str,
) -> Result<String, MarketplaceAddError>
作用:把 marketplace 名字变成安全的本地文件夹名。这样可以防止名字里带斜杠、特殊符号或类似路径跳转的内容,导致文件装错地方。
数据流:进去的是用户或配置里给出的 marketplace 名字。它逐个字符检查:英文字母、数字、横线、下划线、点号会保留,其他字符会替换成横线;然后去掉开头和结尾的点号。如果处理后为空,或者变成危险的 ..,就返回错误。成功时出来的是一个可以拿来当目录名的字符串。
调用关系:它会被 add_marketplace_sync_with_cloner 调用,通常发生在真正创建安装路径之前。它负责先把“外部给来的名字”清洗干净,让后面的安装流程不必直接相信原始名字。
调用图:被 1 处调用(add_marketplace_sync_with_cloner);外部调用 2 个(InvalidRequest, format!)。
ensure_marketplace_destination_is_inside_install_root67–97 ↗
fn ensure_marketplace_destination_is_inside_install_root(
install_root: &Path,
destination: &Path,
) -> Result<(), MarketplaceAddError>
作用:确认 marketplace 的最终安装位置确实在允许的安装根目录里面。它的作用像门卫,防止安装过程把文件写到系统其他地方。
数据流:进去的是安装根目录和准备安装到的目标路径。它会把安装根目录解析成真实路径,也会解析目标目录的父目录真实路径;然后检查目标父目录是不是位于安装根目录之下。如果不是,就返回“请求无效”的错误;如果路径解析本身失败,则返回内部错误。成功时不产出新数据,只表示这条路径安全可用。
调用关系:它会被 add_marketplace_sync_with_cloner 在替换或写入正式安装目录前调用。它不创建目录,也不下载东西,只负责做安全边界检查,避免后面的 replace_marketplace_root 把内容移动到错误位置。
调用图:被 1 处调用(add_marketplace_sync_with_cloner);外部调用 4 个(canonicalize, parent, InvalidRequest, format!)。
replace_marketplace_root99–107 ↗
fn replace_marketplace_root(
staged_root: &Path,
destination: &Path,
) -> std::io::Result<()>
作用:把已经准备好的暂存目录移动到正式安装目录。可以理解成先在后台装好,最后一次性把成品摆上货架。
数据流:进去的是暂存目录路径和正式目标路径。它先确保正式目标目录的父目录存在;如果父目录还没有,就创建出来。然后用文件系统的重命名操作把暂存目录改名或移动到目标位置。成功时文件已经出现在正式安装目录;失败时返回系统的输入输出错误。
调用关系:它会被 add_marketplace_sync_with_cloner 在下载和检查完成后调用。前面的流程负责准备暂存内容和确认路径安全;它只做最后的落地动作。
调用图:被 1 处调用(add_marketplace_sync_with_cloner);外部调用 3 个(parent, create_dir_all, rename)。
marketplace_staging_root109–111 ↗
fn marketplace_staging_root(install_root: &Path) -> PathBuf
作用:给安装流程算出暂存目录的位置。暂存目录就是先临时放半成品的地方,避免安装到一半就污染正式目录。
数据流:进去的是 marketplace 的安装根目录。它在这个根目录下面拼出一个 .staging 子目录路径,然后把这个路径返回。它不访问磁盘,也不创建目录,只是算路径。
调用关系:它会被 add_marketplace_sync_with_cloner 调用,用来决定下载和准备文件时先放在哪里。后续流程会把这个暂存目录交给下载逻辑,最后再通过 replace_marketplace_root 移到正式位置。
调用图:被 1 处调用(add_marketplace_sync_with_cloner);外部调用 1 个(join)。
run_git113–137 ↗
fn run_git(args: &[&str], cwd: Option<&Path>) -> Result<(), MarketplaceAddError>
作用:统一执行 Git 命令,并把成功或失败转成安装流程能理解的结果。它还会禁止 Git 弹出交互式提示,避免程序在后台运行时卡住。
数据流:进去的是一组 Git 参数,比如 clone、checkout,以及可选的工作目录。它创建一个 git 子进程,把参数传进去,设置 GIT_TERMINAL_PROMPT=0 来关闭终端询问;如果指定了工作目录,就在那个目录里运行。命令成功时返回成功;命令失败时,它会读取标准输出和错误输出,拼成一段详细错误信息返回。
调用关系:它只被 clone_git_source 调用,是所有 Git 操作的底层出口。clone_git_source 决定要跑哪些 Git 命令,run_git 负责真正启动命令、收集结果,并把失败原因带回上层。
调用图:被 1 处调用(clone_git_source);外部调用 4 个(from_utf8_lossy, new, Internal, format!)。
core-plugins/src/marketplace_upgrade/activation.rs源码 ↗
插件市场升级时,通常会先把新内容放在一个临时目录里,确认没问题后再“转正”。这个文件就是做这件事的。它会在安装目录里写一个隐藏的 JSON 文件,也就是普通文本格式的安装小票,记录来源、分支、稀疏路径和具体版本号。下次升级前,可以先读这张小票,判断当前安装的内容是不是已经符合要求,避免重复干活。真正激活新目录时,它像搬家一样操作:如果旧目录存在,先搬到备份位置;再把新目录搬到正式位置;如果中途失败,就尽量把旧目录搬回来。还有一个 after_activate 回调,也就是“转正后再做的收尾动作”。如果收尾失败,它同样会尝试回滚。这个文件最重要的地方,是把文件系统上的替换操作做得更稳,减少升级失败后留下半成品的风险。
installed_marketplace_metadata_matches22–43 ↗
fn installed_marketplace_metadata_matches(
root: &Path,
marketplace: &ConfiguredGitMarketplace,
revision: &str,
) -> bool
作用:检查某个已安装的插件市场,是否和当前想要安装的配置、版本完全一致。有人会用它来判断“是不是已经装好了”,从而避免重复升级。
数据流:进去的是安装根目录、市场配置和目标版本号。它先去根目录下读取那张隐藏的安装小票,再把 JSON 文本解析成结构化数据;如果文件不存在、读不了或格式坏了,就直接认为不匹配。最后它把读到的小票和根据当前配置临时生成的小票做比较,返回 true 或 false。
调用关系:它由 upgrade_configured_git_marketplace 在升级流程中调用,通常用在正式升级前的判断环节。它自己会请 installed_marketplace_metadata_path 算出小票文件位置,再请 installed_marketplace_metadata 生成“应该长什么样”的小票;如果解析失败,还会记录一条警告,方便排查。
调用图:调用 2 个内部函数(installed_marketplace_metadata, installed_marketplace_metadata_path);被 1 处调用(upgrade_configured_git_marketplace);外部调用 2 个(read_to_string, warn!)。
write_installed_marketplace_metadata45–55 ↗
fn write_installed_marketplace_metadata(
root: &Path,
marketplace: &ConfiguredGitMarketplace,
revision: &str,
) -> Result<(), String>
作用:把这次已安装插件市场的来源和版本信息写进安装目录。它相当于给这次安装贴上一张可追溯的标签。
数据流:进去的是安装根目录、市场配置和实际版本号。它先生成一份安装小票,再把它转换成排版友好的 JSON 文本,最后写到根目录下的隐藏文件里。成功时返回空结果;如果转成 JSON 或写文件失败,就返回一段说明原因的错误文字。
调用关系:它由 upgrade_configured_git_marketplace 在升级完成时调用,用来记录最终状态。它把生成小票的工作交给 installed_marketplace_metadata,把决定文件路径的工作交给 installed_marketplace_metadata_path,然后通过文件写入把记录落到磁盘上。
调用图:调用 2 个内部函数(installed_marketplace_metadata, installed_marketplace_metadata_path);被 1 处调用(upgrade_configured_git_marketplace);外部调用 2 个(to_string_pretty, write)。
activate_marketplace_root57–150 ↗
fn activate_marketplace_root(
destination: &Path,
staged_dir: TempDir,
after_activate: impl FnOnce() -> Result<(), String>,
) -> Result<(), String>
作用:把临时准备好的新插件市场目录,正式替换成当前使用的目录。它特别在意失败后的回滚,也就是出错时尽量恢复旧目录。
数据流:进去的是目标安装位置、装着新内容的临时目录,以及一个激活后要执行的收尾函数。它先确认目标目录的父目录存在;如果目标位置已有旧版本,就先把旧版本搬到临时备份目录,再把新版本搬过去。新版本搬好后会执行收尾函数;如果搬迁或收尾失败,它会删除新目录并尝试把旧目录恢复回来。最后成功返回空结果,失败返回带细节的错误文字。
调用关系:它由 upgrade_configured_git_marketplace 在真正切换版本时调用,是升级流程里最关键的“换门牌”步骤。它主要调用系统文件操作,比如创建目录、重命名目录、删除目录和创建临时备份目录;after_activate 则让调用者在新目录已经就位后补做自己的动作。
调用图:被 1 处调用(upgrade_configured_git_marketplace);外部调用 8 个(exists, parent, path, format!, create_dir_all, remove_dir_all, rename, new)。
installed_marketplace_metadata152–163 ↗
fn installed_marketplace_metadata(
marketplace: &ConfiguredGitMarketplace,
revision: &str,
) -> InstalledMarketplaceMetadata
作用:根据市场配置和版本号,组装出一份标准的安装小票。它保证读取、写入和比较时使用的是同一种格式。
数据流:进去的是市场配置和版本号。它从配置里取出来源地址、引用名和稀疏路径,再固定标记来源类型为 Git,最后加上版本号,产出一个 InstalledMarketplaceMetadata 数据对象。它不读写文件,只负责把信息整理成统一形状。
调用关系:它被 installed_marketplace_metadata_matches 用来生成“期望的小票”,也被 write_installed_marketplace_metadata 用来生成“要写入的小票”。这样比较和写入走同一套组装逻辑,减少两边格式不一致的风险。
调用图:被 2 处调用(installed_marketplace_metadata_matches, write_installed_marketplace_metadata)。
installed_marketplace_metadata_path165–167 ↗
fn installed_marketplace_metadata_path(root: &Path) -> PathBuf
作用:算出安装小票应该放在哪个文件路径下。它把“隐藏小票文件名”固定在一个地方,避免各处自己拼路径拼错。
数据流:进去的是插件市场的安装根目录。它在这个目录后面接上固定文件名 .codex-marketplace-install.json,出来的是完整路径。它不检查文件是否存在,也不读写文件。
调用关系:它被 installed_marketplace_metadata_matches 和 write_installed_marketplace_metadata 调用。前者用它找到要读取的小票,后者用它找到要写入的小票;这样读和写天然指向同一个位置。
调用图:被 2 处调用(installed_marketplace_metadata_matches, write_installed_marketplace_metadata);外部调用 1 个(join)。
执行和进程支持
本组提供共享执行工具层、进程抽象、输出缓冲、沙箱 exec 辅助工具,以及启动和监督命令时使用的退出状态转换。
core/src/tools/mod.rs源码 ↗
这份文件像一个工具区的前台。项目里有很多和“工具调用”有关的部件,比如路由、沙箱、网络审批、生命周期、事件等,这里先把它们统一声明出来,别的代码才能按同一个入口找到它们。除此之外,它还做几件很实际的小事:把带命名空间的工具名压成老系统能识别的一段字符串;把用户正在用的命令行环境,比如 Bash、Zsh、PowerShell,翻译成工具协议里的类型;把命令执行后的退出码、耗时和输出内容整理给模型看。输出可能很长,所以这里会按截断策略裁短,避免把日志或模型上下文塞爆。还有一个容易忽略的点:如果命令超时,它会在输出前面补一句“命令超时了”,这样读结果的人或模型不会误以为只是普通输出不完整。
flat_tool_name36–46 ↗
fn flat_tool_name(tool_name: &ToolName) -> Cow<'_, str>
作用:把一个结构化的工具名变成一段普通字符串。这样做是为了兼容一些老接口,因为那些地方还不能理解“命名空间加名称”这种分开的工具名。
数据流:进去的是一个 ToolName,里面可能有 namespace(命名空间,可以理解成工具所属的前缀)和 name(真正的工具名)。如果有命名空间,它就新建一个字符串,把命名空间和工具名拼在一起;如果没有,就直接借用原来的工具名字符串。出来的是一个 Cow 字符串,也就是“能借就借、必须新建才新建”的结果,尽量少复制数据。
调用关系:它通常在工具名要跨到旧边界时被调用,比如打遥测指标、请求审批、运行工具、分发工具结果、生成 hook 工具名、生成网络审批说明等。正常排序和比较仍然应该用 ToolName 本身,只有这些老接口需要扁平字符串时才用它。
调用图:被 7 处调用(emit_metric_for_tool_read, request_approval, run, dispatch_any_with_terminal_outcome, function_hook_tool_name, network_approval_spec, network_approval_spec);外部调用 3 个(Borrowed, Owned, with_capacity)。
tool_user_shell_type48–58 ↗
fn tool_user_shell_type(
user_shell: &crate::shell::Shell,
) -> codex_tools::ToolUserShellType
作用:把项目内部认识的用户 shell 类型,翻译成工具协议认识的 shell 类型。shell 可以理解成用户输入命令的环境,比如 Bash 或 PowerShell。
数据流:进去的是用户当前 shell 的信息。函数查看里面的 shell_type,然后逐一匹配:Zsh 变成工具协议里的 Zsh,Bash 变成 Bash,PowerShell 变成 PowerShell,Sh 和 Cmd 也同理。出来的是 codex_tools 里的 ToolUserShellType,供工具调用侧使用。
调用关系:它在准备工具运行上下文时会被用到,比如 make_turn_context 会用它把用户环境写进上下文,spawn_review_thread 也会用它让审查线程知道当前命令环境。它不执行命令,只负责做类型翻译。
调用图:被 2 处调用(spawn_review_thread, make_turn_context)。
format_exec_output_for_model62–87 ↗
fn format_exec_output_for_model(
exec_output: &ExecToolCallOutput,
truncation_policy: TruncationPolicy,
) -> String
作用:把一次命令执行的结果整理成适合发给模型阅读的文本。它会包含退出码、耗时、输出内容,并且在输出太长时安全截断。
数据流:进去的是 ExecToolCallOutput,也就是一次命令运行后的完整结果,以及一个 TruncationPolicy(截断策略,告诉函数最多保留多少内容)。它先把耗时四舍五入到 0.1 秒,再通过 build_content_with_timeout 拿到正文,如果命令超时正文会先带超时提示。然后统计总行数,按策略截断正文,最后拼出“Exit code”“Wall time”“Total output lines”(如果确实被截断了)和“Output”等段落。出来的是一整段给模型看的字符串。
调用关系:这是给模型汇报命令结果时使用的格式化入口。它把补超时提示的工作交给 build_content_with_timeout,把裁短文本的工作交给 truncate_text,自己负责把元信息和正文组装成清楚的报告。
调用图:调用 1 个内部函数(build_content_with_timeout);外部调用 3 个(new, truncate_text, format!)。
format_exec_output_str89–97 ↗
fn format_exec_output_str(
exec_output: &ExecToolCallOutput,
truncation_policy: TruncationPolicy,
) -> String
作用:把命令执行输出整理成一段纯正文,并按规则截断。相比 format_exec_output_for_model,它不额外加退出码和耗时这些标题,更适合需要“只拿输出内容”的地方。
数据流:进去的是命令执行结果和截断策略。它先调用 build_content_with_timeout 得到输出正文;如果命令超时,正文开头会有超时说明。然后调用 formatted_truncate_text 按策略裁剪,并保留格式化后的截断提示。出来的是一段处理好的字符串,不改动原始执行结果。
调用关系:它被 execute_user_shell_command、user_shell_command_fragment 这类需要展示或拼接命令输出的流程调用,也被 includes_timed_out_message 这样的检查使用。它把是否加超时提示交给 build_content_with_timeout,自己专注于把正文裁成合适长度。
调用图:调用 1 个内部函数(build_content_with_timeout);被 3 处调用(includes_timed_out_message, execute_user_shell_command, user_shell_command_fragment);外部调用 1 个(formatted_truncate_text)。
build_content_with_timeout100–110 ↗
fn build_content_with_timeout(exec_output: &ExecToolCallOutput) -> String
作用:取出命令执行的输出正文,并在命令超时时补上一句清楚的提示。这样后面无论谁展示输出,都不会漏掉“这是超时结果”这个关键信息。
数据流:进去的是一次命令执行结果。它检查 timed_out 标记:如果为真,就生成一段新文本,开头写明“command timed out after 多少 milliseconds”,后面接原始输出;如果没有超时,就直接复制原始聚合输出文本。出来的是可以继续截断或展示的正文字符串。
调用关系:它是两个格式化函数 format_exec_output_for_model 和 format_exec_output_str 的共同底层步骤。上层函数负责决定输出长什么样、要不要带元信息;它只负责保证正文里包含必要的超时说明。
调用图:被 2 处调用(format_exec_output_for_model, format_exec_output_str);外部调用 1 个(format!)。
core/src/unified_exec/head_tail_buffer.rs源码 ↗
运行外部命令时,输出可能非常多,比如日志刷屏。如果全存下来,程序可能占用太多内存;如果只存最后一段,又会丢掉最早的关键信息。这个文件里的 HeadTailBuffer 就像一本太厚的书只保留“前几页”和“后几页”:前半容量留给 head,也就是开头;后半容量留给 tail,也就是结尾。新数据进来时,先填满开头区域,之后的数据进入结尾区域;如果结尾区域也满了,就从结尾区域最老的内容开始删,保证总大小不超过上限。它还会记录被省略了多少字节,方便测试或提示用户“中间有内容被丢掉”。这个缓存可以按块取快照、拼成完整字节串,也可以一次性清空并取走内容。
HeadTailBuffer::default21–23 ↗
fn default() -> Self
作用:用系统预设的最大输出大小,创建一个默认的 HeadTailBuffer。别人不想自己决定容量时,就用这个默认版本。
数据流:进去没有额外参数,只读取统一执行模块里的默认最大字节数 → 把这个数字交给创建函数 → 出来一个已经按默认容量分好“开头区”和“结尾区”的缓存。
调用关系:它是最方便的入口之一。执行命令的流程 exec_command 会用它来收集输出,测试也会用它检查默认行为是否能同时保留前缀和后缀。它把真正的初始化工作交给 HeadTailBuffer::new。
调用图:被 5 处调用(head_tail_buffer_default_preserves_prefix_and_suffix, push_chunk_preserves_prefix_and_suffix, new, exec_command, failed_initial_end_for_unstored_process_uses_fallback_output);外部调用 1 个(new)。
HeadTailBuffer::new31–44 ↗
fn new(max_bytes: usize) -> Self
作用:按指定的最大字节数创建一个新缓存。它会把容量大致一分为二:一半留给开头,一半留给结尾。
数据流:进去一个 max_bytes,也就是最多保留多少字节 → 计算 head_budget 和 tail_budget,并准备两个空队列分别放开头块和结尾块 → 出来一个空的 HeadTailBuffer,已记录好容量上限和当前已保存字节数为 0。
调用关系:这是所有自定义容量缓存的起点。默认构造会调用它,测试也会直接调用它来验证各种边界情况,比如容量为 0、单个块太大、清空后状态是否复原。
调用图:被 6 处调用(chunk_larger_than_tail_budget_keeps_only_tail_end, draining_resets_state, fills_head_then_tail_across_multiple_chunks, head_budget_zero_keeps_only_last_byte_in_tail, keeps_prefix_and_suffix_when_over_budget, max_bytes_zero_drops_everything);外部调用 1 个(new)。
HeadTailBuffer::retained_bytes49–51 ↗
fn retained_bytes(&self) -> usize
作用:告诉调用者现在实际还保留了多少字节。它只算还在缓存里的开头和结尾,不算已经丢掉的中间内容。
数据流:进去的是当前缓存自身的状态 → 把 head_bytes 和 tail_bytes 相加,并用安全加法避免数字溢出 → 出来一个字节数,表示目前还能取出来的内容大小。
调用关系:它主要给 HeadTailBuffer::to_bytes 使用,用来提前准备合适大小的输出空间。测试也可以用它确认缓存没有超过限制。
调用图:被 1 处调用(to_bytes)。
HeadTailBuffer::omitted_bytes56–58 ↗
fn omitted_bytes(&self) -> usize
作用:告诉调用者有多少字节因为容量限制被丢掉了。这个数字能说明输出中间被省略了多少。
数据流:进去的是当前缓存自身的状态 → 直接读取 omitted_bytes → 出来一个累计丢弃字节数,不改变缓存内容。
调用关系:它是一个查看状态的小工具,主要用于测试或诊断。真正增加这个数字的地方在 HeadTailBuffer::push_chunk、HeadTailBuffer::push_to_tail 和 HeadTailBuffer::trim_tail_to_budget。
HeadTailBuffer::push_chunk65–91 ↗
fn push_chunk(&mut self, chunk: Vec<u8>)
作用:把一段新的输出字节放进缓存。它决定这段内容应该进入“开头区”、被拆成开头和结尾,还是直接进入“结尾区”。
数据流:进去一段 Vec<u8> 字节块 → 如果总容量是 0,就整段计入已省略;如果开头区还没满,就先填开头区,必要时把一块拆成两半;剩余部分交给结尾区 → 出来没有返回值,但缓存里的内容、已保存字节数和已省略字节数会被更新。
调用关系:这是外部持续喂输出数据时最常用的函数。它自己处理开头区,遇到需要维护结尾区时,会把工作交给 HeadTailBuffer::push_to_tail。
调用图:调用 1 个内部函数(push_to_tail);外部调用 1 个(push_back)。
HeadTailBuffer::snapshot_chunks97–102 ↗
HeadTailBuffer::to_bytes108–117 ↗
fn to_bytes(&self) -> Vec<u8>
作用:把当前保留的所有输出拼成一整段字节。调用者如果不关心原来分成多少块,就可以用它。
数据流:进去的是当前缓存自身 → 先用 HeadTailBuffer::retained_bytes 算出需要多大的空间,再按顺序把 head 和 tail 的每个块拼进去 → 出来一个 Vec<u8>,里面只有保留下来的开头和结尾。
调用关系:它是把缓存内容变成最终可用输出的接口之一。它依赖 HeadTailBuffer::retained_bytes 来预分配空间,但不会修改缓存。
调用图:调用 1 个内部函数(retained_bytes);外部调用 2 个(with_capacity, iter)。
HeadTailBuffer::drain_chunks123–130 ↗
fn drain_chunks(&mut self) -> Vec<Vec<u8>>
作用:把缓存里还保留的块全部取走,并把缓存恢复成空状态。它适合“取完就重来”的场景。
数据流:进去的是可修改的缓存自身 → 先把 head 里的块全部抽走,再把 tail 里的块抽走,然后把 head_bytes、tail_bytes 和 omitted_bytes 都重置为 0 → 出来一组按开头到结尾排列的字节块。
调用关系:它位于一次收集结束后的收尾阶段。和 snapshot_chunks 不同,它会清空内部状态,让同一个 HeadTailBuffer 可以继续用于下一轮输出收集。
调用图:外部调用 1 个(drain)。
HeadTailBuffer::push_to_tail132–157 ↗
fn push_to_tail(&mut self, chunk: Vec<u8>)
作用:把数据放进“结尾区”,并保证结尾区不会超过自己的容量。结尾区的原则是:只保留最新的末尾内容。
数据流:进去一段应该进入 tail 的字节块 → 如果 tail 容量为 0,就整段记为省略;如果这段本身比 tail 容量还大,就只保留它最后那一截,并丢掉旧 tail;否则先追加到 tail 后面,再修剪超出的旧内容 → 出来没有返回值,但 tail 内容、tail_bytes 和 omitted_bytes 会变化。
调用关系:它只由 HeadTailBuffer::push_chunk 调用,是维护“结尾区”的内部零件。追加后如果可能超限,它会继续调用 HeadTailBuffer::trim_tail_to_budget 来剪掉多余的旧数据。
调用图:调用 1 个内部函数(trim_tail_to_budget);被 1 处调用(push_chunk);外部调用 2 个(clear, push_back)。
HeadTailBuffer::trim_tail_to_budget159–178 ↗
fn trim_tail_to_budget(&mut self)
作用:修剪结尾区,让它重新回到容量限制以内。它会从最老的结尾内容开始删,尽量保住最新的输出。
数据流:进去的是当前 tail 已经可能超限的缓存状态 → 计算超出了多少字节,然后从 tail 队列最前面的旧块开始删除;如果只需要删掉一个块的一部分,就只切掉那一段 → 出来没有返回值,但 tail_bytes 变小,omitted_bytes 增加,tail 内容不再超预算。
调用关系:它是 HeadTailBuffer::push_to_tail 的后续清理步骤。外部不会直接调用它;它像自动裁纸刀一样,在新输出追加到结尾区后,把太旧、太多的部分裁掉。
调用图:被 1 处调用(push_to_tail);外部调用 2 个(front_mut, pop_front)。
execpolicy/src/executable_name.rs源码 ↗
这份代码像是给程序名字做“标准化登记”。在检查一个可执行文件是否命中策略规则时,名字必须先变成统一格式,否则同一个程序换个大小写或后缀就可能被当成另一个程序。这里最特别的是 Windows:它会把名字转成小写,并去掉常见可执行后缀,比如 .exe、.cmd、.bat、.com。这样 Git.EXE 和 git 会用同一个查找钥匙。非 Windows 系统则不做这些改动,因为那里的文件名大小写和后缀习惯不同。文件里还有一个从完整路径里取文件名的函数,比如从 /usr/bin/git 或 C:\Tools\git.exe 里拿出最后的 git.exe,再交给名字标准化函数处理。如果路径最后一段不是正常文字,它会返回空结果,避免拿乱码去匹配规则。
executable_lookup_key6–23 ↗
fn executable_lookup_key(raw: &str) -> String
作用:把一个原始的程序名变成“用来查规则的标准名字”。在 Windows 上,它会忽略大小写,并去掉常见可执行文件后缀;在其他系统上,它保持原样。
数据流:进去的是一个程序名字符串,比如 Git.EXE。如果运行在 Windows,它先把名字变成小写,再检查末尾是不是 .exe、.cmd、.bat、.com,是的话就切掉这个后缀,最后输出类似 git 的查找键;如果不是 Windows,就直接输出原来的字符串副本。它不改动外部状态,只返回一个新的字符串。
调用关系:它是这个文件里的核心小工具,负责真正的名字标准化。executable_path_lookup_key 从路径里取出文件名后,会把这个文件名交给它加工;最终这些标准名字会被上层的规则匹配流程拿去比较。
executable_path_lookup_key25–29 ↗
fn executable_path_lookup_key(path: &Path) -> Option<String>
作用:从一个完整路径里取出最后的文件名,并把它变成可用于规则匹配的标准名字。有人拿到的是路径而不是单独的程序名时,就会用它。
数据流:进去的是一个路径对象,比如 /usr/bin/git 或 C:\Tools\Git.EXE。它先调用路径工具取最后一段文件名,再尝试把这个文件名当作正常文本读取;如果成功,就交给 executable_lookup_key 做标准化并返回 Some(名字);如果路径没有文件名,或者文件名不是合法文本,就返回 None。它不修改路径本身。
调用关系:它处在“路径”和“规则匹配”之间,像一个翻译员。调用图里显示它会调用外部的 file_name 来取文件名,并会被 match_host_executable_rules 使用;也就是说,当系统要判断某个宿主机上的可执行文件是否命中规则时,会先通过它把路径整理成可比较的名字。
调用图:被 1 处调用(match_host_executable_rules);外部调用 1 个(file_name)。
linux-sandbox/src/exec_util.rs源码 ↗
这段代码主要服务于“执行另一个程序”这件事。Rust 里的字符串不能直接交给 Linux 的底层执行接口,因为底层要的是 C 字符串(一种以特殊零字节结尾的老式字符串),所以这里提供了转换工具。另一个重点是文件描述符,也就是系统给打开文件发的“号码牌”。默认有些号码牌带着 CLOEXEC 标记,意思是“启动新程序时自动关掉”。但沙箱启动工具有时必须把某些文件继续交给子进程用,所以这里会把这个标记清掉。可以把它想成搬家前整理钥匙:哪些钥匙要带到新房,必须提前从“自动丢弃”清单里拿出来。文件底部的测试专门确认这个行为真的生效。
argv_to_cstrings5–14 ↗
fn argv_to_cstrings(argv: &[String]) -> Vec<CString>
作用:把 Rust 的命令行参数列表转换成 C 字符串列表。这样后面调用 Linux 的执行接口时,参数格式才是系统能看懂的。
数据流:输入是一组 Rust 字符串参数。它逐个尝试转成 CString,也就是 C 风格字符串;如果某个参数里面有不允许出现的内部零字节,就直接报错并停止。输出是一组转换好的 CString,供真正启动程序时使用。
调用关系:它会在 exec 和 exec_system_bwrap 准备启动外部命令时被调用。它只负责把参数打包好,不负责真正启动程序;打包后的结果会交给后续执行流程使用。
调用图:被 2 处调用(exec, exec_system_bwrap);外部调用 3 个(new, with_capacity, panic!)。
make_files_inheritable16–20 ↗
fn make_files_inheritable(files: &[File])
作用:让指定的已打开文件可以被即将启动的新程序继承。没有这一步,某些需要传给子进程的文件可能会在 exec 时被系统自动关闭。
数据流:输入是一组 File,也就是已经打开的文件对象。它拿到每个文件背后的文件描述符号码,然后交给 clear_cloexec 去去掉“执行新程序时关闭”的标记。它没有返回值,但会改变这些文件描述符的系统标志。
调用关系:exec 和 exec_system_bwrap 在启动程序前会用它处理需要保留的文件。测试 tests::preserved_files_are_made_inheritable 也会调用它,确认它确实能让文件变成可继承。
调用图:调用 1 个内部函数(clear_cloexec);被 3 处调用(exec, preserved_files_are_made_inheritable, exec_system_bwrap)。
clear_cloexec22–40 ↗
fn clear_cloexec(fd: libc::c_int)
作用:清掉某个文件描述符上的 FD_CLOEXEC 标记。FD_CLOEXEC 的意思是“执行新程序时自动关闭这个文件”,清掉后子进程才能继续拿到它。
数据流:输入是一个文件描述符号码。它先用 fcntl 读取当前标志;如果读取失败,就带着系统错误信息崩溃。然后它把 FD_CLOEXEC 那一位关掉;如果本来就没开,就什么也不做。最后它用 fcntl 写回新标志,失败同样会报错停止。
调用关系:它是 make_files_inheritable 背后的实际干活者。外部流程不会直接批量处理系统标志,而是通过 make_files_inheritable 间接调用它,保证每个要保留的文件都被正确处理。
调用图:被 1 处调用(make_files_inheritable);外部调用 3 个(last_os_error, fcntl, panic!)。
tests::preserved_files_are_made_inheritable49–56 ↗
fn preserved_files_are_made_inheritable()
作用:这是一个测试,用来证明 make_files_inheritable 真的会去掉文件的 FD_CLOEXEC 标记。它防止以后改代码时不小心破坏这个关键行为。
数据流:它先创建一个临时文件,再故意给这个文件设置 FD_CLOEXEC 标记。接着调用 make_files_inheritable。最后重新读取文件描述符标志,检查 FD_CLOEXEC 已经变成 0,也就是不会在新程序启动时自动关闭。
调用关系:它在测试运行时执行,不参与真实沙箱启动。它会用 tests::set_cloexec 准备测试条件,用 tests::fd_flags 检查结果,中间调用真正的 make_files_inheritable 来验证生产代码。
调用图:调用 1 个内部函数(make_files_inheritable);外部调用 4 个(new, assert_eq!, set_cloexec, from_ref)。
tests::set_cloexec58–66 ↗
fn set_cloexec(fd: libc::c_int)
作用:这是测试用的小帮手,负责给某个文件描述符强行加上 FD_CLOEXEC 标记。这样测试才能模拟“文件原本会被 exec 自动关闭”的情况。
数据流:输入是文件描述符号码。它先通过 tests::fd_flags 读取当前标志,再把 FD_CLOEXEC 这一位加上去,并用 fcntl 写回系统。如果写入失败,就读取系统错误并让测试失败。
调用关系:它只被 tests::preserved_files_are_made_inheritable 用来布置测试现场。它的作用是先制造一个需要修复的问题,然后让 make_files_inheritable 去解决。
调用图:外部调用 4 个(last_os_error, fcntl, fd_flags, panic!)。
tests::fd_flags68–76 ↗
fn fd_flags(fd: libc::c_int) -> libc::c_int
作用:这是测试用的小帮手,用来读取某个文件描述符当前有哪些系统标志。测试靠它确认 FD_CLOEXEC 有没有被设置或清除。
数据流:输入是文件描述符号码。它调用 fcntl 向系统询问这个号码的标志;成功就返回标志值,失败就读取系统错误并让测试停止。
调用关系:它被 tests::set_cloexec 用来在修改前读取旧标志,也被测试流程用来检查最终结果。它不改变任何东西,只负责把系统里的当前状态读出来。
调用图:外部调用 3 个(last_os_error, fcntl, panic!)。
utils/pty/src/process.rs源码 ↗
运行交互式程序时,麻烦不只是“启动它”。还要把键盘输入送进去,把 stdout/stderr(标准输出和标准错误,程序吐出来的两路文字)接出来,记录退出码,并在用户关闭页面或任务结束时把后台任务收干净。这个文件就像一个“进程遥控器”。ProcessHandle 保存写入通道、杀进程的工具、后台读写任务、退出状态和 PTY 句柄。PTY 句柄必须留着,因为有些系统里一旦伪终端的从端被关掉,子进程会收到类似 Ctrl+C 的信号。文件还支持改终端尺寸:如果本地有 PTY,就直接改;如果是别的后端,就调用后端给的 resize 回调。spawn_from_driver 则把外部后端提供的输入、输出、退出信号包装成统一的 SpawnedProcess,方便上层不用关心底层到底是哪种实现。
unsupported_signal26–33 ↗
fn unsupported_signal(signal: ProcessSignal) -> io::Error
作用:当某种进程后端不支持发指定信号时,用这个函数生成一个清楚的错误。比如想发“中断”,但这个后端没有办法做到,就返回“不支持”。
数据流:进去的是一个 ProcessSignal(进程信号,这里主要是 Interrupt,中断)→ 函数判断是哪种信号 → 出来的是一个 io::Error,说明这个后端不支持这个动作,不会改动外部状态。
调用关系:它是各类 signal 实现的兜底工具。ClosureTerminator::signal 会直接调用它,其他 signal 实现也可能在无法支持时借它返回统一错误。
exit_code_from_status35–49 ↗
fn exit_code_from_status(status: ExitStatus) -> i32
作用:把操作系统给的退出状态翻译成普通整数退出码。这样上层不用理解不同系统的细节,只看一个数字就知道程序怎么结束。
数据流:进去的是 ExitStatus(操作系统报告的进程结束状态)→ 先尝试读取正常退出码;在 Unix 上如果是被信号杀掉,就转成 128 加信号编号;都拿不到就用 -1 → 出来是一个 i32 退出码。
调用关系:它会被启动本地进程的流程使用,比如 spawn_process_with_stdin_mode 在等待子进程结束后,用它把系统状态转成统一退出码。
调用图:被 1 处调用(spawn_process_with_stdin_mode);外部调用 2 个(code, signal)。
TerminalSize::default64–66 ↗
fn default() -> Self
作用:给终端大小提供一个默认值:24 行、80 列。这是很多传统终端的常见尺寸,没特别指定时就用它。
数据流:没有输入 → 创建一个 TerminalSize,rows 是 24,cols 是 80 → 返回这个默认终端尺寸,不改动任何状态。
调用关系:很多启动会话或测试流程在没有明确尺寸时会用它,比如 open_session_with_exec_env、start_process 和多种 PTY 相关测试。
调用图:被 8 处调用(open_session_with_exec_env, start_process, pipe_and_pty_share_interface, pty_preserving_inherited_fds_keeps_python_repl_running, pty_python_repl_emits_output_and_exits, pty_spawn_can_preserve_inherited_fds, pty_spawn_with_inherited_fds_reports_exec_failures, pty_terminate_kills_background_children_in_same_process_group)。
PtySize::from70–77 ↗
fn from(value: TerminalSize) -> Self
作用:把本文件里简单的 TerminalSize 转成 portable_pty 库需要的 PtySize。简单说,就是把“行列数”翻译成底层 PTY 能听懂的格式。
数据流:进去的是 TerminalSize,里面有 rows 和 cols → 复制行列数,并把像素宽高设为 0,因为这里按字符格子算大小 → 出来是 PtySize。
调用关系:ProcessHandle::resize 在调整可变大小的 PTY 时会通过 into 间接用到它,把上层尺寸交给 portable_pty 的 resize 方法。
PtyHandles::fmt101–103 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:让 PtyHandles 在调试打印时有一个安全、简洁的显示方式。它不会把底层复杂句柄细节全打印出来。
数据流:进去的是格式化器和 PtyHandles 自身 → 写入一个名叫 PtyHandles 的调试结构 → 输出给调试打印系统,不改变 PTY 句柄。
调用关系:它服务于 Rust 的 Debug 打印机制。有人打印 PtyHandles 时,系统会调用它;它内部只用 debug_struct 生成简短结果。
调用图:外部调用 1 个(debug_struct)。
ProcessHandle::fmt129–131 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
作用:让 ProcessHandle 能被调试打印,同时避免暴露或展开里面一堆通道、锁和后台任务。打印时只显示它是一个 ProcessHandle。
数据流:进去的是格式化器和 ProcessHandle 自身 → 写入一个简短的调试结构名 → 输出给调试打印系统,不改变进程状态。
调用关系:它也是 Debug 机制的一部分。日志或调试工具打印 ProcessHandle 时会走这里。
调用图:外部调用 1 个(debug_struct)。
ProcessHandle::new136–160 ↗
fn new(
writer_tx: mpsc::Sender<Vec<u8>>,
killer: Box<dyn ChildTerminator>,
reader_handle: JoinHandle<()>,
reader_abort_handles: Vec<AbortHandle>,
writer_handle
作用:把一个交互式进程所需的所有零件装配成 ProcessHandle。就像把遥控器、电源线、屏幕输出线和关闭按钮都装到一个盒子里。
数据流:进去的是写入通道、终止器、读写等待任务、退出状态、退出码、PTY 句柄和可选的改尺寸回调 → 函数把这些东西分别放进锁或共享指针里保存 → 出来是一个可供上层操作的 ProcessHandle。
调用关系:它是创建统一进程句柄的装配点。spawn_from_driver 会调用它,把外部驱动提供的会话包装成统一接口;其他启动流程也可以用它组装本地进程。
调用图:外部调用 1 个(new)。
ProcessHandle::writer_sender163–173 ↗
fn writer_sender(&self) -> mpsc::Sender<Vec<u8>>
作用:拿到一个可以往子进程 stdin(标准输入,程序接收键盘输入的入口)写数据的发送器。调用者用它把字节送进正在运行的程序。
数据流:进去的是 ProcessHandle 自身 → 它尝试锁住内部保存的 writer_tx;如果还开着,就克隆一个发送器返回;如果已经关闭或锁失败,就造一个马上失效的空通道发送器 → 出来是 mpsc::Sender<Vec<u8>>。
调用关系:上层需要给子进程发送输入时会用它。它只负责提供入口,真正的写入工作由创建会话时保存的后台 writer 任务完成。
ProcessHandle::has_exited176–178 ↗
fn has_exited(&self) -> bool
作用:快速查看子进程是不是已经退出。它适合给界面或控制逻辑判断“这个会话还活着吗”。
数据流:进去的是 ProcessHandle 自身 → 读取一个 AtomicBool(原子布尔值,可以被多个任务安全读取的真假标记)→ 返回 true 或 false,不修改状态。
调用关系:等待进程结束的后台任务会设置这个标记;其他地方通过这个函数读取它,避免直接接触底层共享变量。
ProcessHandle::exit_code181–183 ↗
fn exit_code(&self) -> Option<i32>
作用:读取已经知道的退出码。如果进程还没结束,或者退出码还没写入,就返回空。
数据流:进去的是 ProcessHandle 自身 → 锁住保存退出码的 Mutex(互斥锁,一把锁,防止多个任务同时改同一份数据)→ 如果里面有数字就返回 Some(code),否则返回 None。
调用关系:等待退出的任务会把退出码写进去;界面、测试或上层流程会通过这个函数查询结果。
ProcessHandle::resize186–210 ↗
fn resize(&self, size: TerminalSize) -> anyhow::Result<()>
作用:调整交互式终端的行数和列数。比如用户把终端窗口拖大了,就需要告诉子进程新的大小,否则全屏程序可能显示错位。
数据流:进去的是新的 TerminalSize → 先尝试锁住本地 PTY 句柄;如果有可直接调整的 master,就调用它的 resize;如果 Unix 上只有原始文件描述符,就走 resize_raw_pty;如果没有本地 PTY,就尝试调用外部后端给的 resizer 回调 → 出来是成功或错误。
调用关系:它是上层改窗口大小时调用的统一入口。它可能把活交给 portable_pty 的 resize、Unix 专用的 resize_raw_pty,或驱动后端传入的 resize 回调。
调用图:调用 1 个内部函数(resize_raw_pty);外部调用 3 个(lock, anyhow!, into)。
ProcessHandle::close_stdin213–217 ↗
fn close_stdin(&self)
作用:关闭子进程的输入通道。意思是告诉程序“不会再给你输入了”,有些程序会因此结束读取并退出。
数据流:进去的是 ProcessHandle 自身 → 锁住 writer_tx,把里面保存的发送器取走并丢掉 → 之后再正常获取输入发送器就不会连到原来的 stdin 通道。
调用关系:上层在不想再发送输入、或需要模拟文件结束时会调用它。它只关闭输入通道,不主动杀进程。
调用图:外部调用 1 个(lock)。
ProcessHandle::request_terminate221–227 ↗
ProcessHandle::signal229–238 ↗
fn signal(&self, signal: ProcessSignal) -> io::Result<()>
作用:给子进程发送一个指定信号,比如 Interrupt(中断,类似按 Ctrl+C)。如果底层后端不支持,就返回错误或什么也不做。
数据流:进去的是 ProcessSignal → 函数锁住 killer;如果终止器还在,就调用它的 signal;如果锁失败或终止器已经没了,就直接返回成功 → 出来是 io::Result,表示信号是否成功交给后端。
调用关系:上层想模拟 Ctrl+C 这类操作时会用它。真正怎么发信号由具体 ChildTerminator 决定;像 ClosureTerminator 这种简单终止器会通过 unsupported_signal 表示不支持。
调用图:外部调用 1 个(lock)。
ProcessHandle::terminate241–264 ↗
fn terminate(&self)
作用:彻底收掉这个进程会话:先请求杀掉子进程,再取消相关后台任务。它用于用户关闭会话或句柄被丢弃时的清理。
数据流:进去的是 ProcessHandle 自身 → 先调用 request_terminate;然后依次锁住 reader、额外 reader、writer、wait 这些任务句柄,并调用 abort 取消它们 → 没有返回值,但会改变内部状态,让这些任务不再继续跑。
调用关系:ProcessHandle::drop 会自动调用它,保证句柄被释放时不会留下后台任务。也可以由上层主动调用来立即清理。
调用图:调用 1 个内部函数(request_terminate);被 1 处调用(drop);外部调用 1 个(lock)。
ProcessHandle::drop268–270 ↗
fn drop(&mut self)
作用:这是 ProcessHandle 被销毁时自动执行的收尾动作。它防止调用者忘记手动关闭进程,导致后台任务或子进程泄漏。
数据流:进去的是即将被释放的 ProcessHandle → 调用 terminate → 结果是子进程被请求结束,相关后台任务被取消。
调用关系:它由 Rust 的资源释放机制自动调用。核心工作交给 ProcessHandle::terminate。
调用图:调用 1 个内部函数(terminate)。
ClosureTerminator::signal279–281 ↗
fn signal(&mut self, signal: ProcessSignal) -> io::Result<()>
作用:这是一个简单终止器的“发信号”实现,但它本身不支持任何具体信号。有人要求它发中断时,它会返回“不支持”。
数据流:进去的是 ProcessSignal → 调用 unsupported_signal 生成对应错误 → 出来是 io::Result 的错误,不改动内部闭包。
调用关系:ClosureTerminator 用在 spawn_from_driver 里,把一个简单的关闭回调包装成 ChildTerminator。因为这个回调只会 kill,不懂 signal,所以这里统一报不支持。
调用图:调用 1 个内部函数(unsupported_signal)。
ClosureTerminator::kill283–288 ↗
fn kill(&mut self) -> io::Result<()>
作用:调用里面保存的关闭回调,用来结束外部驱动提供的进程或会话。它把“怎么关”这件事交给创建它的人。
数据流:进去的是 ClosureTerminator 自身 → 如果 inner 里有闭包,就执行这个闭包;如果没有,就什么也不做 → 返回成功。
调用关系:ProcessHandle::request_terminate 会通过 ChildTerminator 接口调用它。spawn_from_driver 会把 driver 里的 terminator 放进 ClosureTerminator。
resize_raw_pty292–304 ↗
fn resize_raw_pty(raw_fd: RawFd, size: TerminalSize) -> anyhow::Result<()>
作用:在 Unix 系统上,用底层系统调用直接调整一个 PTY 的窗口大小。它处理的是更靠近操作系统的原始句柄。
数据流:进去的是 raw_fd(原始文件描述符,可以理解成操作系统给某个打开资源的编号)和 TerminalSize → 组装 libc::winsize,并调用 ioctl(Unix 系统调用,用来控制设备)设置窗口大小 → 成功返回 Ok,失败返回操作系统错误。
调用关系:ProcessHandle::resize 在遇到 Opaque 类型的 PTY master 时会调用它。也就是说,本地库不能直接 resize 时,就用这个 Unix 后门完成。
调用图:被 1 处调用(resize);外部调用 2 个(last_os_error, ioctl)。
combine_output_receivers307–339 ↗
fn combine_output_receivers(
mut stdout_rx: mpsc::Receiver<Vec<u8>>,
mut stderr_rx: mpsc::Receiver<Vec<u8>>,
) -> broadcast::Receiver<Vec<u8>>
作用:把 stdout 和 stderr 两路输出合并成一路广播输出。这样订阅者不用分别监听两根管子,只收一个合并后的流。
数据流:进去的是两个 mpsc 接收器,分别接 stdout 和 stderr 的字节块 → 函数创建一个 broadcast(广播通道,一份消息可以给多个接收者)并启动后台任务;后台任务同时等两路输入,哪边来数据就转发到合并通道;两边都关闭后退出 → 返回合并通道的接收器。
调用关系:它是输出管道的小型转接器。调用者拿到返回的 broadcast::Receiver 后,就能接收两路合并后的输出;实际合并工作在 tokio::spawn 启动的异步任务里进行。
spawn_from_driver362–456 ↗
fn spawn_from_driver(driver: ProcessDriver) -> SpawnedProcess
作用:把一个“外部驱动”提供的进程连接,包装成项目统一使用的 SpawnedProcess。这样不管进程是不是本地 PTY 启动的,上层都能用同一套接口操作。
数据流:进去的是 ProcessDriver,里面有写入通道、stdout/stderr 广播接收器、退出码接收器、可选终止回调、写任务和改尺寸回调 → 函数创建新的 stdout/stderr mpsc 通道,启动后台任务把驱动的广播输出转发进去;再启动等待退出码的任务,记录退出状态和退出码,并通知输出转发任务;最后用 ProcessHandle::new 组装会话 → 出来是 SpawnedProcess,包含统一的 session、stdout_rx、stderr_rx 和 exit_rx。
调用关系:它是非标准后端接入这套 PTY/process 接口的入口。它自己负责搭桥:输出转发交给 tokio::spawn 的 reader 任务,退出状态交给 wait 任务,最终通过 ProcessHandle::new 交给上层使用。
调用图:外部调用 8 个(clone, new, new, new, new, new, spawn, channel)。
windows-sandbox-rs/src/unified_exec/backends/windows_common.rs源码 ↗
Windows 沙箱里跑命令时,外层程序不能像本地进程那样直接把键盘输入、屏幕输出都接上,所以需要一套中转办法。这个文件就像一个小型电话总机:一边把用户输入打包成带格式的消息,通过管道送给沙箱里的 runner;另一边从 runner 读回输出、错误和退出码,再分发给等待的人。这里的“管道”可以理解成两个程序之间传纸条的通道,“帧消息”就是每张纸条都有固定格式,避免读错。文件还处理了 Windows 终端换行的特殊习惯,把单独的换行补成 Windows 常用的回车加换行。它也提供调整终端大小的发送器,以及在 runner 出错或管道提前断开时,把错误文字送到标准错误或标准输出里,避免外层程序无声失败。
finish_driver_spawn20–26 ↗
fn finish_driver_spawn(driver: ProcessDriver, stdin_open: bool) -> SpawnedProcess
作用:这个函数把已经准备好的进程驱动器真正启动起来,并按需要决定是否立刻关闭标准输入。有人用它是为了统一处理“进程已启动,但输入口要不要保持打开”这一步。
数据流:进去的是一个 ProcessDriver(可以理解成一张启动进程的施工单)和一个 stdin_open 标记。它先用这张施工单生成真正的 SpawnedProcess;如果标记说标准输入不该保持打开,就马上关闭这个输入口。出来的是已经启动好的进程对象,同时它可能已经把这个进程的输入口关掉了。
调用关系:它处在 Windows 沙箱会话启动的末尾。spawn_windows_sandbox_session_elevated_for_permission_profile 和 spawn_windows_sandbox_session_legacy 在准备好启动参数后会调用它;它把真正启动进程的活交给 spawn_from_driver,然后把整理好的进程交回上层继续使用。
调用图:被 2 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile, spawn_windows_sandbox_session_legacy);外部调用 1 个(spawn_from_driver)。
normalize_windows_tty_input28–43 ↗
fn normalize_windows_tty_input(bytes: &[u8], previous_was_cr: &mut bool) -> Vec<u8>
作用:这个函数专门修正 Windows 终端里的换行输入。Windows 终端通常希望换行是“回车加换行”,它会把单独的换行补齐,避免程序收到不习惯的输入格式。
数据流:进去的是一段原始字节,以及一个 previous_was_cr 状态,用来记住上一段输入最后是不是回车。它逐个看字节:遇到单独的换行就先补一个回车;如果前面已经是回车,就不重复补。出来的是修正后的字节,同时 previous_was_cr 会被更新,供下一批输入继续判断。
调用关系:它是标准输入写入流程里的小修理工。start_runner_stdin_writer 在需要规范化换行时会用它先整理输入,再把整理后的内容打包发给 runner。
调用图:外部调用 1 个(with_capacity)。
start_runner_pipe_writer45–57 ↗
fn start_runner_pipe_writer(
mut pipe_write: File,
) -> std::sync::mpsc::Sender<FramedMessage>
作用:这个函数启动一个后台写入工人,专门把程序内部的消息写进通往 runner 的管道。这样主流程只要把消息丢进队列,不用自己卡在那里慢慢写文件管道。
数据流:进去的是一个可写的 File,也就是通向 runner 的写端。它创建一个普通线程间队列,把发送端交出去;后台任务不断从队列收 FramedMessage,然后按固定帧格式写入管道。出来的是一个 Sender,别人拿它就能把消息排队发送;如果管道写失败,后台任务会停下来。
调用关系:它在提权版 Windows 沙箱会话启动时被 spawn_windows_sandbox_session_elevated_for_permission_profile 使用。上层拿到它返回的发送端后,后续的输入、调整窗口大小等消息都可以通过这个发送端进入同一条 runner 管道。
调用图:被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 1 个(spawn_blocking)。
start_runner_stdin_writer59–94 ↗
fn start_runner_stdin_writer(
mut writer_rx: mpsc::Receiver<Vec<u8>>,
outbound_tx: std::sync::mpsc::Sender<FramedMessage>,
normalize_newlines: bool,
stdin_open: bool,
) -> tokio::task:
作用:这个函数启动一个后台工人,把用户或上层程序送来的标准输入转成 runner 能看懂的消息。它还会在输入结束时通知 runner:“标准输入已经关了”。
数据流:进去的是一个接收字节的异步队列、一个发往 runner 的消息发送端、是否修正换行的开关,以及标准输入是否保持打开的标记。它在后台不断取出输入字节,必要时先做 Windows 换行修正,再用 base64 这类安全文本包装方式放进 Stdin 消息并发送。队列结束后,如果原本标准输入是打开的,它会再发一个 CloseStdin 消息。出来的是一个后台任务句柄;实际效果是输入被持续转发,结束时 runner 也会收到关闭输入的信号。
调用关系:它由 spawn_windows_sandbox_session_elevated_for_permission_profile 在建立 runner 通信后启动。它接上游的输入队列,把活交给 start_runner_pipe_writer 产生的发送端;如果需要换行修正,会先经过 normalize_windows_tty_input。
调用图:被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 1 个(spawn_blocking)。
start_runner_stdout_reader96–161 ↗
fn start_runner_stdout_reader(
mut pipe_read: File,
stdout_tx: broadcast::Sender<Vec<u8>>,
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
exit_tx: oneshot::Sender<i32>,
)
作用:这个函数启动一个后台读取线程,专门从 runner 那边读回输出、错误和退出码。没有它,沙箱里的命令即使跑完了,外层也不知道它打印了什么、是否成功退出。
数据流:进去的是 runner 管道的读取端、标准输出广播器、可选的标准错误广播器,以及一个只发送一次退出码的通道。它循环读取一条条帧消息:Output 消息会解码成字节并转发到标准输出或标准错误;Exit 消息会把退出码送出去并结束;Error 或管道异常会生成一条可见错误消息,并用 -1 表示异常退出。出来没有直接返回值,但它会持续向外广播输出,并最终通知退出状态。
调用关系:它由 spawn_windows_sandbox_session_elevated_for_permission_profile 在会话建立时启动,位置相当于 runner 的回声监听员。它从管道读消息,正常输出就分发给订阅者;遇到 runner 报错或管道提前断开时,会调用 send_runner_error 把问题变成用户能看到的文字。
调用图:被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 1 个(spawn)。
make_runner_resizer163–179 ↗
fn make_runner_resizer(
outbound_tx: std::sync::mpsc::Sender<FramedMessage>,
) -> Box<dyn FnMut(TerminalSize) -> Result<()> + Send>
作用:这个函数做出一个“调整终端大小”的小按钮。外层以后只要调用这个按钮并传入新的行列数,它就会把窗口大小变化告诉沙箱里的 runner。
数据流:进去的是发往 runner 的消息发送端。它返回一个可调用的函数;以后这个函数收到 TerminalSize 后,会把 rows 和 cols 放进 Resize 消息并发送出去。如果发送失败,说明 runner 的管道已经关了,就返回一个错误。出来的是这个可反复调用的调整大小函数。
调用关系:它在 spawn_windows_sandbox_session_elevated_for_permission_profile 建立会话时被创建,并交给上层作为终端大小变化时的处理入口。真正发送消息时,它复用前面建立好的 runner 消息发送通道。
调用图:被 1 处调用(spawn_windows_sandbox_session_elevated_for_permission_profile);外部调用 1 个(new)。
send_runner_error181–192 ↗
fn send_runner_error(
message: &str,
stdout_tx: &broadcast::Sender<Vec<u8>>,
stderr_tx: Option<&broadcast::Sender<Vec<u8>>>,
)
作用:这个函数把 runner 相关错误变成用户能看到的一行文字。它的重点是避免后台管道坏掉时只在内部失败,而外面看起来像什么都没发生。
数据流:进去的是错误说明、标准输出广播器,以及可选的标准错误广播器。它把说明加工成“runner error: ...”这种带换行的字节;如果有标准错误通道,就发到那里,否则退而求其次发到标准输出。它没有返回重要结果,但会把错误消息推送给外部读输出的人。
调用关系:它是 start_runner_stdout_reader 的错误出口。读取线程发现管道提前关闭、读取失败,或者 runner 主动发来 Error 消息时,会调用它把内部错误转成可见输出。
cli/src/exit_status.rs源码 ↗
这个文件解决的是“代跑命令后怎么收尾”的问题。CLI 程序有时会启动另一个程序,比如在沙箱里跑一条命令。那条命令结束后,会留下一个退出状态:正常退出时通常有退出码,比如 0 表示成功,非 0 表示失败;在 Unix 系统上,也可能是被信号杀掉,比如用户按了中断键。这个文件把这些情况翻译成当前进程自己的退出码,然后立刻结束当前程序。它的重要性在于,如果不这样做,外层工具、自动化脚本或 CI 系统可能会误以为命令成功了。Unix 上如果子程序是被信号结束的,它会按常见约定返回“128 加信号编号”;Windows 上通常只有普通退出码,少见异常情况就退回到 1,表示失败。
handle_exit_status16–23 ↗
fn handle_exit_status(status: std::process::ExitStatus) -> !
作用:这个函数接收一个子程序的退出状态,然后让当前 CLI 程序用对应的退出码直接结束。有人会在“已经跑完外部命令,准备退出自己”时用它,保证失败不会被偷偷吞掉。
数据流:进去的是一个 ExitStatus,也就是系统告诉我们的“子程序最后怎么结束了”。函数先尝试读取普通退出码;如果有,就用这个码退出当前程序。在 Unix 上,如果没有普通退出码,它再看看是不是被信号结束;如果是,就用“128 加信号编号”的惯例退出。再不行,或者在 Windows 上遇到少见的非普通情况,就用 1 退出,表示失败。出来的结果不是返回一个值,而是当前进程立刻结束。
调用关系:run_command_under_sandbox 在沙箱里的命令跑完后会调用它,把最后的收尾交给它。它自己不再调用项目里的其他业务代码,只调用系统提供的读取退出码、读取信号和结束进程的能力;所以它像一个最后的“翻译员”,把子命令的结局翻译成整个 CLI 的结局。
调用图:被 1 处调用(run_command_under_sandbox);外部调用 3 个(code, signal, exit)。
cli/src/debug_sandbox/pid_tracker.rs源码 ↗
这份代码主要服务 macOS。它像给一个进程家族装了“门铃”:一开始记录根进程,然后用 kqueue(macOS 提供的内核事件队列,可以在进程 fork、退出时通知程序)监听它的变化;同时用 proc_listchildpids(系统接口,用来问“这个父进程现在有哪些孩子”)补查已经存在的子进程。代码维护两本账:seen 是“见过的所有进程号”,active 是“还在监听的进程号”。只要某个进程 fork 出新孩子,就继续给孩子也装上监听,于是能递归追到孙子、重孙子。PidTracker::new 会把这件耗时、阻塞的监听工作放到后台线程里跑;PidTracker::stop 会发一个自定义停止事件,让后台循环收尾并返回见过的进程号集合。这个文件也包含几组测试,确认它能识别当前进程、直接子进程,以及 bash 再拉起的更深层子进程。
PidTracker::new13–22 ↗
fn new(root_pid: i32) -> Option<Self>
作用:创建一个进程追踪器,从指定的根进程号开始盯。外部在启动沙箱子进程后会用它,目的是从一开始就别漏掉后续冒出来的子进程。
数据流:进去的是 root_pid,也就是要追踪的起点进程号;如果这个号不合法,就直接返回 None。合法时,它向系统创建一个 kqueue 监听队列,并把真正的追踪工作丢到 tokio 的 spawn_blocking 后台任务里;出来的是一个 PidTracker,里面保存 kqueue 句柄和后台任务句柄。
调用关系:它是追踪流程的入口之一。测试里的 pid_tracker_collects_spawned_children、pid_tracker_collects_bash_subshell_descendants 会直接用它;真实流程里 on_child_spawn 在子进程启动后也会用它。它自己不做长时间监听,而是把活交给后台运行的 track_descendants。
调用图:被 3 处调用(pid_tracker_collects_bash_subshell_descendants, pid_tracker_collects_spawned_children, on_child_spawn);外部调用 2 个(kqueue, spawn_blocking)。
PidTracker::stop24–27 ↗
async fn stop(self) -> HashSet<i32>
作用:停止追踪,并拿回这段时间见过的所有进程号。调用者通常在沙箱里的子进程结束或准备清理时用它。
数据流:进去的是整个 PidTracker 本身;它先通过 trigger_stop_event 往 kqueue 里塞一个“该停了”的信号,然后等待后台追踪任务结束。出来的是 HashSet<i32>,也就是去重后的 PID 集合;如果后台任务异常结束,就返回空集合。
调用关系:它是 PidTracker::new 开始的后台追踪的收尾动作。它把停止信号交给 trigger_stop_event,真正的监听循环 track_descendants 收到信号后退出并返回结果。
调用图:调用 1 个内部函数(trigger_stop_event)。
list_child_pids39–60 ↗
fn list_child_pids(parent: i32) -> Vec<i32>
作用:查询某个进程当前直接生出的子进程号。它解决的问题是:监听事件可能告诉你“有变化”,但你还得问系统“孩子到底是谁”。
数据流:进去的是父进程号 parent;函数准备一块数组缓冲区,调用 macOS 的 proc_listchildpids 把子 PID 填进去。如果缓冲区不够大,就扩大后重试;如果没有查到或出错,就返回空列表。出来的是这个父进程的直接子进程 PID 列表。
调用关系:watch_children 会调用它来拿到某个父进程的孩子,然后逐个交给 add_pid_watch 继续追踪。测试 list_child_pids_includes_spawned_child 也会用它验证刚启动的 sleep 子进程能被查到。
调用图:被 2 处调用(list_child_pids_includes_spawned_child, watch_children);外部调用 2 个(new, vec!)。
pid_is_alive62–75 ↗
fn pid_is_alive(pid: i32) -> bool
作用:判断一个进程号现在是不是还代表一个活着的进程。它用来避免追踪循环在根进程已经没了之后还傻等。
数据流:进去的是 pid;如果 pid 不合法,直接认为不活着。否则它调用 kill(pid, 0),这不是杀进程,而是向系统询问“这个进程存在吗”。如果系统说存在,或者说存在但当前用户没权限操作,就返回 true;其他情况返回 false。
调用关系:track_descendants 在发现当前没有任何 active 进程可监听时会调用它,确认根进程是不是已经结束。如果根进程也没了,整个追踪循环就可以退出。
调用图:被 1 处调用(track_descendants);外部调用 2 个(kill, matches!)。
watch_pid83–108 ↗
fn watch_pid(kq: libc::c_int, pid: i32) -> Result<(), WatchPidError>
作用:把某个进程加入 kqueue 的监听名单。加入后,系统会在它 fork、exec 或退出时通知这里的追踪循环。
数据流:进去的是 kqueue 句柄和 pid;函数构造一个 kevent 规则,告诉系统要监听这个 pid 的 NOTE_FORK、NOTE_EXEC、NOTE_EXIT。成功时返回 Ok;如果系统说进程不存在,返回 ProcessGone;其他系统错误会包装成 Other。
调用关系:add_pid_watch 会调用它给每个新发现的进程安装监听。它是这套追踪机制连接 macOS 内核通知的关键小零件。
调用图:被 1 处调用(add_pid_watch);外部调用 5 个(Other, last_os_error, kevent, null, null_mut)。
watch_children110–119 ↗
fn watch_children(
kq: libc::c_int,
parent: i32,
seen: &mut HashSet<i32>,
active: &mut HashSet<i32>,
)
作用:查看某个父进程现在有哪些孩子,并把这些孩子也纳入追踪。它让追踪从“只盯一个人”变成“盯住整个家族”。
数据流:进去的是 kqueue、父进程号 parent,以及 seen 和 active 两本账;它先用 list_child_pids 查出直接子进程,然后对每个子进程调用 add_pid_watch。结果是 seen 可能新增见过的 PID,active 可能新增正在监听的 PID。
调用关系:track_descendants 在收到 fork 事件时会调用它,add_pid_watch 在新加一个进程后也会调用它做递归扫描。它把“查孩子”和“继续监听孩子”这两步串起来。
调用图:调用 2 个内部函数(add_pid_watch, list_child_pids);被 2 处调用(add_pid_watch, track_descendants)。
add_pid_watch122–150 ↗
fn add_pid_watch(kq: libc::c_int, pid: i32, seen: &mut HashSet<i32>, active: &mut HashSet<i32>)
作用:把一个进程,以及它已经存在的后代,加入追踪范围。它负责同时更新“见过”和“正在监听”两本账。
数据流:进去的是 kqueue、pid、seen 集合、active 集合;如果 pid 不合法就不做事。它先把 pid 记入 seen,再尝试放入 active 并调用 watch_pid 安装系统监听;如果进程已经消失或监听失败,就从 active 移除。只要这个 pid 是新见到的,或成功装上了监听,它就继续调用 watch_children 去找它的孩子。
调用关系:这是递归追踪的核心函数。track_descendants 用它加入根进程;watch_children 用它加入每个孩子;它又可能回头调用 watch_children 去追更深一层。
调用图:调用 2 个内部函数(watch_children, watch_pid);被 2 处调用(track_descendants, watch_children);外部调用 1 个(warn!)。
register_stop_event153–165 ↗
fn register_stop_event(kq: libc::c_int) -> bool
作用:在 kqueue 里注册一个程序自己能触发的“停止按钮”。没有这个按钮,后台监听可能会一直卡在等系统事件,外面想停也不好停。
数据流:进去的是 kqueue 句柄;它创建一个 EVFILT_USER 类型的 kevent,也就是用户自定义事件,并注册到队列里。注册成功返回 true,失败返回 false。
调用关系:track_descendants 在进入主监听循环前调用它。之后 PidTracker::stop 会通过 trigger_stop_event 触发同一个停止事件,让 track_descendants 知道该退出。
调用图:被 1 处调用(track_descendants);外部调用 3 个(kevent, null, null_mut)。
trigger_stop_event167–182 ↗
fn trigger_stop_event(kq: libc::c_int)
作用:按下前面注册好的“停止按钮”,通知后台追踪循环退出。它让停止过程不用粗暴中断线程。
数据流:进去的是 kqueue 句柄;如果句柄无效就直接返回。否则它向 kqueue 发送 NOTE_TRIGGER,让等待事件的 track_descendants 醒过来并看到停止请求。它不返回有意义的数据,也不会因为触发失败而抛出错误。
调用关系:PidTracker::stop 会调用它。它和 register_stop_event 配成一对:一个先装按钮,一个后来按按钮。
track_descendants185–275 ↗
fn track_descendants(kq: libc::c_int, root_pid: i32) -> HashSet<i32>
作用:真正执行“追踪根进程所有后代”的后台循环。它把前面的查询孩子、安装监听、处理退出和停止信号都组装成完整流程。
数据流:进去的是 kqueue 句柄和 root_pid;如果 kqueue 不可用或停止事件注册失败,它至少返回包含 root_pid 的集合。正常情况下,它创建 seen 和 active 两个集合,先给根进程装监听,然后不断从 kqueue 读事件:fork 时查孩子并继续追,退出时从 active 移除,收到自定义停止事件时跳出循环。结束前关闭 kqueue,出来的是整个过程中见过的 PID 集合。
调用关系:PidTracker::new 会把它放进 spawn_blocking 后台任务里运行。它调用 register_stop_event 准备停止通道,调用 add_pid_watch 建立追踪,必要时用 pid_is_alive 判断根进程是否还在,并在 fork 事件出现时把活交给 watch_children。PidTracker::stop 触发停止事件后,它负责真正退出并交回结果。
调用图:调用 4 个内部函数(add_pid_watch, pid_is_alive, register_stop_event, watch_children);外部调用 6 个(new, last_os_error, close, kevent, zeroed, null)。
tests::pid_is_alive_detects_current_process285–288 ↗
tests::list_child_pids_includes_spawned_child292–315 ↗
fn list_child_pids_includes_spawned_child()
作用:测试 list_child_pids 能查到刚启动的直接子进程。它确认“问系统要孩子名单”这一步在 macOS 上真的可用。
数据流:进去没有外部参数;测试启动一个 /bin/sleep 子进程,拿到子进程号和当前父进程号,然后短时间内反复调用 list_child_pids 查询父进程的孩子。最后杀掉并等待子进程结束,再断言列表里曾经出现过那个子 PID。
调用关系:它只在 macOS 测试中运行。它验证 list_child_pids,而这个函数在正式流程里由 watch_children 调用,是发现新子进程的基础。
调用图:调用 1 个内部函数(list_child_pids);外部调用 6 个(from_millis, null, assert!, new, id, sleep)。
tests::pid_tracker_collects_spawned_children319–343 ↗
async fn pid_tracker_collects_spawned_children()
作用:测试完整的 PidTracker 能收集到运行期间新启动的直接子进程。它比单独测查询函数更接近真实使用场景。
数据流:进去没有外部参数;测试先用当前进程号创建 PidTracker,然后启动一个短暂运行的 /bin/sleep 子进程并等待它结束,最后调用 tracker.stop().await 拿回 seen 集合。它断言集合里既有父进程 PID,也有刚才的子进程 PID。
调用关系:它只在 macOS 的异步测试里运行。它会调用 PidTracker::new 开始后台追踪,再通过 PidTracker::stop 间接让 track_descendants 收尾,用来验证这些零件能一起工作。
tests::pid_tracker_collects_bash_subshell_descendants347–371 ↗
async fn pid_tracker_collects_bash_subshell_descendants()
作用:测试 PidTracker 不只会抓直接子进程,也能抓到子进程再启动的更深层后代。这个场景很重要,因为真实命令经常会通过 shell 再拉起别的程序。
数据流:进去没有外部参数;测试先创建 PidTracker,然后启动 bash,让 bash 在里面后台运行 sleep 并把那个 sleep 的 PID 打印出来。测试读取输出、解析出这个更深层子进程号,再停止 tracker,最后断言 seen 集合包含这个 PID。
调用关系:它只在 macOS 的异步测试里运行。它调用 PidTracker::new 启动追踪,并用停止流程拿回结果,验证 track_descendants、watch_children、add_pid_watch 的递归追踪链条没有只停在第一层。
调用图:调用 1 个内部函数(new);外部调用 6 个(null, piped, from_utf8_lossy, assert!, new, id)。
编辑和补丁应用辅助工具
这些工具支持交互式编辑流程,以及上层文件更新流程使用的模糊补丁上下文匹配。
tui/src/external_editor.rs源码 ↗
这个文件解决的是“在终端界面里写长文本不方便”的问题。程序先从环境变量里找用户设置的编辑器:优先看 VISUAL,没有再看 EDITOR。环境变量可以理解成操作系统给程序看的小纸条,里面写着用户偏好的工具。找到后,它会把原始内容写进一个临时的 .md 文件,再启动编辑器打开这个文件。用户保存并退出后,程序检查编辑器是不是正常结束,然后把文件里的新内容读回来。这里还特别照顾了 Windows:有些命令其实是 .cmd 或 .bat 包装脚本,普通启动方式可能找不到,所以会额外解析。测试部分主要保证环境变量选择规则正确,以及临时编辑流程真的能把内容改回来。
resolve_windows_program25–29 ↗
fn resolve_windows_program(program: &str) -> std::path::PathBuf
作用:这个函数只在 Windows 上用,用来把用户写的编辑器命令变成真正能运行的程序路径。这样像 code 这种命令,即使实际是 code.cmd,也能被找到。
数据流:进去的是一个程序名,比如“code” → 它用系统路径 PATH 和 Windows 的可执行扩展名规则 PATHEXT 去查找真实文件 → 找到了就返回完整路径,找不到就原样返回这个名字,交给后面启动命令时再处理。
调用关系:它是 run_editor 在 Windows 上启动编辑器前的垫脚石。run_editor 需要创建进程,但 Windows 对 .cmd、.bat 这类命令不总是自动识别,所以先把查找工作交给 resolve_windows_program。
调用图:被 1 处调用(run_editor);外部调用 1 个(which)。
resolve_editor_command33–51 ↗
fn resolve_editor_command() -> std::result::Result<Vec<String>, EditorError>
作用:这个函数决定到底要用哪个外部编辑器。它优先读取 VISUAL,其次读取 EDITOR,并把一整串命令拆成程序名和参数。
数据流:它从操作系统环境变量里读取 VISUAL 或 EDITOR → 按当前系统的命令写法把字符串拆开,比如把“code --wait”拆成“code”和“--wait” → 如果没设置、拆不开或拆完为空,就返回明确的错误;成功时返回一组命令片段。
调用关系:它通常会被更上层的 launch_external_editor 调用,用来在真正打开编辑器前先确定命令。测试 resolve_editor_prefers_visual 也会调用它,确认 VISUAL 确实比 EDITOR 优先。
调用图:被 2 处调用(launch_external_editor, resolve_editor_prefers_visual);外部调用 3 个(var, split, split)。
run_editor54–91 ↗
async fn run_editor(seed: &str, editor_cmd: &[String]) -> Result<String>
作用:这个函数真正执行“打开外部编辑器让用户改文字”这件事。它把初始文字放进临时文件,启动编辑器,等用户退出后再把编辑后的文字读出来。
数据流:进去的是初始内容 seed 和已经拆好的编辑器命令 editor_cmd → 它创建一个临时 Markdown 文件,把 seed 写进去,然后启动编辑器并把这个临时文件路径作为参数传给编辑器 → 编辑器正常退出后,它读取临时文件的新内容并返回;如果命令为空、编辑器启动失败或退出状态不正常,就返回错误。
调用关系:它是在已经知道编辑器命令之后执行的核心步骤。Windows 上它会先调用 resolve_windows_program 来修正程序路径;测试 run_editor_returns_updated_content 会用一个假的小脚本当编辑器,验证它确实能拿到改过的内容。
调用图:调用 1 个内部函数(resolve_windows_program);被 1 处调用(run_editor_returns_updated_content);外部调用 7 个(new, msg, inherit, new, format!, read_to_string, write)。
tests::EnvGuard::new107–112 ↗
fn new() -> Self
作用:这个测试辅助函数会记住测试开始前 VISUAL 和 EDITOR 原本是什么。这样测试临时修改环境变量后,不会污染其他测试或开发者的终端环境。
数据流:它读取当前的 VISUAL 和 EDITOR → 把读到的值保存进 EnvGuard 这个小对象里 → 返回这个对象,等测试结束时用来恢复现场。
调用关系:它被环境变量相关测试在开头创建。它和 tests::EnvGuard::drop 配合,就像测试前先拍一张现场照片,测试后按照片复原。
调用图:外部调用 1 个(var)。
tests::EnvGuard::drop116–119 ↗
fn drop(&mut self)
作用:这个函数在 EnvGuard 被丢弃时自动运行,用来恢复 VISUAL 和 EDITOR。Drop 是 Rust 的自动清理机制,可以理解成对象退场前的收拾动作。
数据流:它拿到 EnvGuard 里保存的旧 VISUAL 和 EDITOR → 分别交给 restore_env → 最后环境变量回到测试前的状态。
调用关系:它不需要测试手动调用,测试函数结束时会自动触发。它把具体恢复动作交给 tests::restore_env,让每个变量按原来有值或没值的情况恢复。
调用图:外部调用 1 个(restore_env)。
tests::restore_env122–127 ↗
fn restore_env(key: &str, value: Option<String>)
作用:这个测试辅助函数把某个环境变量恢复到指定状态:原来有值就设回去,原来没有就删掉。
数据流:进去的是环境变量名和一个可选的旧值 → 如果旧值存在,它就把变量设回这个值;如果旧值不存在,它就把变量移除 → 结果是这个环境变量恢复成测试前的样子。
调用关系:它被 tests::EnvGuard::drop 调用,是测试清理现场的具体执行者。这样多个测试改 VISUAL、EDITOR 时,不会互相影响。
调用图:外部调用 2 个(remove_var, set_var)。
tests::resolve_editor_prefers_visual131–139 ↗
fn resolve_editor_prefers_visual()
作用:这个测试确认当 VISUAL 和 EDITOR 都设置时,程序会优先选择 VISUAL。这很重要,因为很多系统约定 VISUAL 表示更适合交互式编辑的编辑器。
数据流:它先保存原环境变量 → 临时把 VISUAL 设成“vis”,把 EDITOR 设成“ed” → 调用 resolve_editor_command → 检查返回结果是不是只选择了“vis”。
调用关系:它直接验证 resolve_editor_command 的优先级规则。EnvGuard 在测试结束时自动恢复环境变量,避免这个测试影响后面的测试。
调用图:调用 1 个内部函数(resolve_editor_command);外部调用 3 个(assert_eq!, set_var, new)。
tests::resolve_editor_errors_when_unset143–153 ↗
fn resolve_editor_errors_when_unset()
作用:这个测试确认如果 VISUAL 和 EDITOR 都没有设置,程序会给出“没有编辑器”的错误,而不是随便猜一个或悄悄失败。
数据流:它先保存原环境变量 → 临时删除 VISUAL 和 EDITOR → 调用 resolve_editor_command → 检查结果是不是 EditorError::MissingEditor 这个明确错误。
调用关系:它验证 resolve_editor_command 的失败路径。这个行为能帮助上层界面给用户显示清楚的提示:你还没有配置外部编辑器。
tests::run_editor_returns_updated_content157–170 ↗
async fn run_editor_returns_updated_content()
作用:这个测试确认 run_editor 的完整流程能跑通:临时文件被编辑器改掉后,函数能读回新内容。它只在 Unix 类系统上运行。
数据流:它创建一个临时目录和一个假的编辑器脚本 → 脚本做的事很简单:把传进来的文件内容改成“edited” → 测试调用 run_editor,传入初始内容“seed”和这个脚本命令 → 最后检查返回值是不是“edited”。
调用关系:它从外部模拟了一个真实编辑器,直接测试 run_editor。这样不用真的打开 Vim 或 VS Code,也能确认写临时文件、启动程序、读取结果这一整条链路是正常的。
调用图:调用 1 个内部函数(run_editor);外部调用 6 个(assert_eq!, metadata, set_permissions, write, tempdir, vec!)。
apply-patch/src/seek_sequence.rs源码 ↗
给文件打补丁时,程序通常要先在原文件里找到补丁描述的那几行“上下文”。这个文件就是做这件事的:给它原文件的所有行、要找的几行内容、从哪里开始找,它会返回匹配到的位置。它像是在文章里找一句话,但不是只认一模一样的字。它会先尝试完全一致;不行就忽略行尾空格;再不行就连行首空格也忽略;最后还会把花体引号、长破折号、不间断空格这类容易混淆的 Unicode 字符换成普通 ASCII 字符再试一次。ASCII 可以理解成最常见的英文键盘字符集合。这里还特别处理了两个容易出错的边界:要找的内容为空时,直接认为在起点匹配;要找的行数比文件还多时,直接返回找不到,避免程序崩溃。文件下半部分是测试,确认这些宽松匹配和防崩溃行为都符合预期。
seek_sequence12–110 ↗
fn seek_sequence(
lines: &[String],
pattern: &[String],
start: usize,
eof: bool,
) -> Option<usize>
作用:在文本行列表里寻找一段连续的目标行,并返回它从第几行开始。它是补丁应用过程里的“定位器”:先帮程序找到位置,后面才能安全地替换或插入内容。
数据流:输入是原文件的行列表、要寻找的目标行列表、开始搜索的位置,以及是否优先从文件末尾匹配。它先处理空目标和目标过长这两种特殊情况,然后按从严格到宽松的顺序搜索:完全相同、忽略行尾空白、忽略两端空白、再把常见花式标点和特殊空格统一成普通字符后比较。找到就输出匹配开始的行号;始终找不到就输出 None,表示没有匹配位置。它不改动输入的文本,只负责判断位置。
调用关系:它由 compute_replacements 调用。整体流程是:上游先算出补丁想找的上下文,再把这些行交给 seek_sequence;seek_sequence 找到位置后,上游才能继续计算真正要替换哪些行。它自己不调用项目里的其他函数,主要在函数内部完成多轮比较。
调用图:被 1 处调用(compute_replacements)。
tests::to_vec117–119 ↗
fn to_vec(strings: &[&str]) -> Vec<String>
作用:把测试里写得很方便的字符串切片转换成真正的 String 列表。这样测试用例可以少写重复代码,更清楚地表达“这些就是文件里的几行”。
数据流:输入是一组字符串引用,比如几行简短文本。它逐个复制成 String,最后输出一个 Vec<String>,也就是可交给 seek_sequence 使用的行列表。它不做匹配,也不改变外部状态。
调用关系:它服务于本文件里的多个测试函数。测试先用 tests::to_vec 准备 lines 和 pattern,再调用 seek_sequence,最后用断言检查结果是否正确。
tests::test_exact_match_finds_sequence122–129 ↗
fn test_exact_match_finds_sequence()
作用:确认最基本的情况能工作:目标内容和原文完全一样时,函数能找到正确位置。这是整个匹配功能的底线。
数据流:它先把 “foo、bar、baz” 做成原文件行列表,再把 “bar、baz” 做成要找的目标行。然后调用 seek_sequence 从开头搜索,最后检查结果是不是 Some(1),也就是从第二行开始匹配。测试只验证结果,不修改真实文件。
调用关系:这是 seek_sequence 的基础测试之一。它通过 tests::to_vec 准备数据,并用 assert_eq! 检查返回值,确保后续那些宽松匹配没有破坏最简单的精确匹配。
调用图:外部调用 2 个(to_vec, assert_eq!)。
tests::test_rstrip_match_ignores_trailing_whitespace132–140 ↗
fn test_rstrip_match_ignores_trailing_whitespace()
作用:确认行尾多出来的空格或制表符不会导致匹配失败。制表符可以理解成键盘 Tab 打出来的空白。
数据流:它准备的原文行末尾带有额外空白,而目标行不带这些空白。然后调用 seek_sequence 搜索,并检查它仍然返回 Some(0),表示从第一行开始匹配成功。这个测试证明函数会在精确匹配失败后尝试忽略行尾空白。
调用关系:它是 seek_sequence 宽松匹配能力的测试。流程上,它先用 tests::to_vec 造数据,再交给 seek_sequence,最后用 assert_eq! 验证“忽略行尾空白”这一步确实生效。
调用图:外部调用 2 个(to_vec, assert_eq!)。
tests::test_trim_match_ignores_leading_and_trailing_whitespace143–151 ↗
fn test_trim_match_ignores_leading_and_trailing_whitespace()
作用:确认行首缩进和行尾空白都不完全一致时,函数仍有机会找到目标。这样补丁不会因为多几个空格就轻易失败。
数据流:它准备的原文件行前后都有额外空白,而目标行只写核心文字。调用 seek_sequence 后,它检查返回值是 Some(0)。这说明函数在更宽松的一轮比较里,会把两边的空白都去掉再判断。
调用关系:它验证 seek_sequence 的第三层匹配策略。测试数据由 tests::to_vec 生成,结果由 assert_eq! 检查,用来保证“忽略首尾空白”的行为稳定存在。
调用图:外部调用 2 个(to_vec, assert_eq!)。
tests::test_pattern_longer_than_input_returns_none154–162 ↗
fn test_pattern_longer_than_input_returns_none()
作用:确认要找的内容比原文件还长时,函数会安全地返回找不到,而不是崩溃。这是一个防护性测试,专门保护边界情况。
数据流:它准备一个只有一行的原文件,却准备三行目标内容。调用 seek_sequence 后,期望结果是 None。这个过程验证函数会先判断目标是否可能放进原文里,不可能就提前结束。
调用关系:它守住 seek_sequence 的安全边界。测试通过 tests::to_vec 准备短输入和长目标,再用 assert_eq! 确认返回 None,避免以后改代码时重新引入越界读取之类的崩溃问题。
调用图:外部调用 2 个(to_vec, assert_eq!)。