Git 状态与自动刷新
本页记录 ZCode 中与工作区 Git 状态读取、分支解析和只读 Git 世界读取相关的实现。当前可见源码能够完整证明 Git 快照与 Git 只读命令构造;“自动刷新”若指商店目录自动刷新,则属于另一套市场能力,本页仅作边界说明,不将其与 Git 状态轮询混为一谈。
Purpose and Scope
本页覆盖以下范围:
resolveGitSnapshot()如何判断工作区是否为 Git 仓库,并读取当前分支、主分支、Git 用户、工作区状态和最近提交。- Git 子进程的超时、输出大小限制、失败降级与 provider-visible context 的安全处理。
- Git world-read 只读操作的设计原则:固定 argv、工作区范围、路径归一化、ref/path 校验,以及未跟踪文件的处理。
- CLI 工作区分支解析
resolveWorkspaceGitBranch()的独立、轻量实现。
本页不覆盖插件商店目录的 Catalog Auto-Refresh、Manual Refresh 或插件生命周期。术语定义明确指出,目录自动刷新是进入商店页时对 Official Marketplace 的节流后台刷新,而手动刷新是顶栏按钮触发的全市场刷新;现有 Git 源码并未证明二者由 Git 状态模块驱动。因此,若要了解商店刷新策略,应查看对应的商店/目录页面,而不是将其推断为 Git 自动刷新。
Overview
Git 相关读取被拆成几个层次:
git-snapshot.ts面向环境上下文,执行一组低成本 Git 查询并返回Partial<EnvInfo>。它适合把仓库状态注入运行时上下文。workflow-git-world-read.ts的前半部分是纯 argv 构造与解析契约。它强调“只读是构造出来的”,而不是先构造任意命令、再依赖运行时检查阻止写操作。tui-workspace-git.ts面向 TUI 工作区,只读取当前分支;它使用更短的超时和更小的输出上限,并将 detached HEAD 或任何失败转换为undefined。
这些实现共同遵循两个关键约束:Git 命令始终通过参数数组执行,而不是拼接 shell 字符串;工作区相对路径必须被约束在当前工作区范围内。这样做的目的不是提供 Git 写入能力,而是让状态展示、上下文注入和只读 world-read 的边界在代码结构上可验证。
Architecture
架构中的 Snapshot、Plan、Validation、Parser 和 Branch 分别对应已读取源码中的导出函数或其所在模块职责。git 是唯一外部进程边界;工作区目录通过 cwd 或 -C 传入。需要注意,图中没有“自动刷新调度器”:在本页取到的源码中没有足够证据证明 Git 状态会被定时轮询或由商店页面刷新触发。
Git 快照读取
resolveGitSnapshot() 的控制流
resolveGitSnapshot(workingDirectory) 首先执行 git rev-parse --is-inside-work-tree。只有输出精确为 true 才继续;否则返回 { isGitRepository: false, gitStatus: "not_repo" }。这一步把“不是仓库”作为正常状态,而不是异常抛出,调用方可以直接决定是否展示 Git 信息。
在仓库内,函数依次读取:
- 当前分支:
rev-parse --abbrev-ref HEAD;空结果回退为HEAD。 - 主分支:优先读取
refs/remotes/origin/HEAD,然后依次探测远程main、master;没有匹配时回退为main。 - Git 用户:
git config user.name。 - 工作区状态:
git --no-optional-locks status --short。 - 最近提交:
git --no-optional-locks log --oneline -n 5。
状态文本会先去掉首尾空白,再由 splitGitStatusForContext() 按换行拆分。空列表表示 clean,非空列表表示 dirty。原始状态上下文最多保留 2,000 个字符;超出时追加提示,告知调用方如果需要更多信息,应通过 Bash 或 Windows 上的 PowerShell 执行 git status。这是一种上下文预算控制,而不是对 Git 文件条目数的限制。
失败降级和安全边界
所有 Git 子命令都经过 execGitNoThrow()。执行配置固定为:工作目录是传入的 workingDirectory,编码为 UTF-8,最大缓冲区为 1 MiB,超时为 3,000 ms,并启用 windowsHide。命令失败时返回空 stdout/stderr,不把可能不可靠的错误输出写入 provider-visible Git context。结果是单项信息可以缺失,但快照流程不因单个查询失败而泄露 stderr 或直接中断。
1export async function resolveGitSnapshot(workingDirectory: string): Promise<Partial<EnvInfo>> {
2 const insideWorkTree = await readTrimmedGitOutput(
3 ["rev-parse", "--is-inside-work-tree"],
4 workingDirectory,
5 );
6 if (insideWorkTree !== "true") {
7 return {
8 isGitRepository: false,
9 gitStatus: GIT_STATUS_NOT_REPO,
10 };
11 }
12
13 const gitBranch = await resolveGitBranch(workingDirectory);
14 const gitMainBranch = await resolveMainBranch(workingDirectory);
15 const gitUser = await resolveGitUser(workingDirectory);
16 const statusOutput = await readGitOutput(
17 ["--no-optional-locks", "status", "--short"],
18 workingDirectory,
19 );
20 const gitStatusLines = splitGitStatusForContext(statusOutput?.trim() ?? "");
21
22 return {
23 isGitRepository: true,
24 gitBranch,
25 gitMainBranch,
26 gitUser,
27 gitStatus: gitStatusLines.length > 0 ? GIT_STATUS_DIRTY : GIT_STATUS_CLEAN,
28 gitStatusLines,
29 };
30}Source: git-snapshot.ts
Git world-read:固定命令与工作区边界
workflow-git-world-read.ts 将 Git world-read 的纯部分与真正 spawn Git 的执行侧分开。纯部分只生成五类允许的只读操作的 argv,并负责校验参数。这样测试可以直接断言“某个操作恰好生成哪些参数”,无需通过 fake process 间接验证。
Ref 校验
validateGitRef() 拒绝空 ref、以 - 开头的值、包含 .. 的范围表达式,以及不符合 /^[A-Za-z0-9][A-Za-z0-9._/@^~-]*$/ 的值。v1 只承诺单个 ref,不接受 reflog、ref:path 或范围语法。拒绝 -foo 的关键原因是避免 Git 将其解释成选项;拒绝其他语法则是为了保持明确的 API 语义,而不是宣称这些语法本身都不安全。
Path 校验
validateGitPath() 要求路径非空、非绝对、首字符不是 -,并先把反斜杠转换为 /,再拒绝包含 .. 段的路径。绝对路径判断同时覆盖 /、反斜杠开头和 Windows 盘符形式,因此不会依赖运行文档进程的操作系统来决定输入是否有效。所有路径最终位于 -- 之后;首字符 - 仍被拒绝属于纵深防御。
changedFiles 的语义
没有 base 时,gitChangedFilesPlan() 返回两条命令的计划:对 HEAD 的已跟踪差异,以及遵守 .gitignore 的未跟踪文件列表。这样新建 feature 分支、文件尚未被 Git 跟踪时,调用方仍能看到对读者有意义的改动。提供 base 时只生成 diff --name-only <base>,因为该模式表达的是相对某个 ref 的已跟踪历史差异。
1export function gitChangedFilesPlan(base: string | undefined): GitCommandPlan[] {
2 const ref = base === undefined ? "HEAD" : validateGitRef("git-changed-files", base);
3 const tracked: GitCommandPlan = {
4 argv: ["diff", "--name-only", "-z", ref, "--", WORKSPACE_PATHSPEC],
5 };
6 if (base !== undefined) return [tracked];
7 return [tracked, { argv: [...GIT_UNTRACKED_ARGV] }];
8}Source: workflow-git-world-read.ts
输出解析与路径一致性
world-read 使用 -z 让 Git 以 NUL 分隔路径,避免非 ASCII 路径被 C 引用,也避免文件名中的换行破坏按行解析。路径列表统一转换为仓库根相对路径;ls-files --others 额外使用 --full-name,以补齐它默认输出 cwd 相对路径的差异。工作区如果只是仓库子目录,则通过 git rev-parse --show-prefix 获得前缀并剥除,使 changedFiles() 的结果与 files.read() 等工作区范围操作一致。
补丁文本是明确例外:git.diff 的输出含有 a/...、b/... 头,模块不把它当路径列表解析,因此使用 Git 自身的 --relative 对齐工作区基准,而不是事后剥前缀。工作区范围的代价也被源码明确写出:当工作区是仓库子目录时,v1 有意看不到子目录之外的改动。
TUI 工作区分支读取
resolveWorkspaceGitBranch() 是比完整快照更窄的 API:它只尝试读取工作区当前分支,不读取 status、log 或用户配置。调用方可以注入 runCommand,因此分支解析本身可在不启动真实 Git 进程的情况下测试。
默认超时为 750 ms,stdout 上限为 512 bytes。命令以 git -C <workspaceDirectory> symbolic-ref --quiet --short HEAD 执行;spawn 异常、超时、非零退出码、空输出以及 detached HEAD 都返回 undefined。成功时只取第一行并去除空白。
1export async function resolveWorkspaceGitBranch(options: {
2 maxOutputBytes?: number;
3 runCommand?: GitCommandRunner;
4 timeoutMs?: number;
5 workspaceDirectory: string;
6}): Promise<string | undefined> {
7 const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_GIT_OUTPUT_LIMIT_BYTES;
8 const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
9 const runCommand = options.runCommand ?? runGitCommand;
10 const result = await runCommand(
11 GIT_COMMAND,
12 ["-C", options.workspaceDirectory, ...GIT_BRANCH_ARGS],
13 {
14 maxOutputBytes,
15 timeoutMs,
16 },
17 ).catch(() => ({ exitCode: -1, stdout: "" }));
18
19 if (result.exitCode !== 0) return undefined;
20
21 const branch = result.stdout.trim().split(/\r?\n/, 1)[0]?.trim();
22 if (!branch || branch === DETACHED_HEAD_LABEL) return undefined;
23 return branch;
24}Source: tui-workspace-git.ts
API Reference
resolveGitSnapshot(workingDirectory: string): Promise<Partial<EnvInfo>>
- 参数:
workingDirectory,作为 Git 命令的cwd。 - 返回值:非仓库时返回
isGitRepository: false与gitStatus: "not_repo";仓库内返回仓库标识、分支、主分支、Git 用户、clean/dirty状态、状态行和最多五条最近提交。类型是Partial<EnvInfo>,因此部分字段可以因单项命令失败而缺失。 - 失败行为:Git 命令失败被吞并为空结果;源码未声明异常向上传播。
gitChangedFilesPlan(base?: string): GitCommandPlan[]
- 参数:可选的单个 Git ref。未传入时默认以
HEAD为比较基准,并额外包含遵守.gitignore的未跟踪文件;传入时仅生成相对该 ref 的已跟踪差异命令。 - 返回值:只读命令计划数组;计划对象的
argv不包含git可执行文件名。 - 失败行为:空 ref、选项样式 ref、范围 ref、非法字符会抛出
WorkflowError("DriverError", ...)。源码片段显示该异常用于让脚本根据明确原因改写输入。
resolveWorkspaceGitBranch(options): Promise<string | undefined>
- 参数:
workspaceDirectory必填;timeoutMs和maxOutputBytes可选;runCommand可注入。 - 返回值:普通分支名;失败、detached HEAD 或空输出时为
undefined。 - 失败行为:命令拒绝、spawn 错误、超时和输出超限都被转换为失败结果,不抛出 Git 执行异常。
Configuration Options
这些实现没有从配置文件读取的运行时配置键;以下是源码中的常量默认值:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
| Git snapshot command timeout | number | 3000 ms | git-snapshot.ts 中每次 execFile 的超时 |
| Git snapshot max buffer | number | 1048576 bytes | Git snapshot 子进程 stdout/stderr 缓冲上限 |
| Git status context limit | number | 2000 字符 | 注入上下文的状态文本上限 |
| Recent commits limit | number | 5 | 最近提交数量上限 |
| TUI branch timeout | number | 750 ms | TUI 分支读取超时 |
| TUI branch output limit | number | 512 bytes | TUI 分支读取 stdout 上限 |
源码没有显示环境变量或配置文件覆盖这些常量;因此不应把它们描述为用户可配置项。
Core Flow
Sources:
Failure Modes、边界与并发
- 非仓库目录:完整快照返回明确的
not_repo,而 TUI 分支 API 返回undefined;两者语义不同,调用方不应把后者当成“肯定不是仓库”。 - Detached HEAD:TUI API 明确隐藏 detached HEAD;完整快照的
rev-parse --abbrev-ref HEAD则会得到HEAD,因为两套 API 的产品用途不同。 - Git 不存在或命令超时:两套执行器都以失败结果继续,而不是将 Git 不可用升级为整个调用失败。
- 输出过大:完整快照用 1 MiB 进程缓冲,同时把 status context 限制为 2,000 字符;TUI 只允许 512 bytes。二者解决的是不同层面的预算问题。
- 路径越界与参数注入形状:world-read 在执行前拒绝绝对路径、
..段、前导-和非法 ref,并把路径放到--后;这使只读边界成为 argv 构造的性质。 - 非 ASCII 或换行文件名:world-read 使用 NUL 分隔,避免常规换行解析静默地产生错误路径。
- 并发:已读取源码没有显示全局缓存、锁、去抖器或自动刷新调度。每次调用都会独立发起 Git 查询;因此不能从这些文件推断存在并发合并或刷新节流策略。
Performance / Operational Notes
完整快照最多发起多次短 Git 命令,单命令超时 3 秒;TUI 分支读取则设计为更快的 750 ms。状态上下文和最近提交都有限长,避免将完整仓库状态或无限日志注入模型上下文。--no-optional-locks 用于 status 和 log,减少只读查询对 Git 锁行为的影响。
运维上,调用方应把缺失字段视为可接受状态,并在需要完整信息时调用 Git 工具获取最新结果;源码生成的 2,000 字符截断提示已经明确建议使用 Bash 或 PowerShell。当前源码没有自动重试、缓存 TTL 或定时刷新实现证据。
Extension Points
- 为
resolveWorkspaceGitBranch()注入runCommand,可以测试非零退出、超时、输出超限和 detached HEAD,而无需依赖本机 Git 状态。 - 若扩展 world-read 操作,应保持“纯计划构造 / 执行侧 spawn”的分层,并复用 ref/path 校验、
--分隔和工作区 pathspec 约束。 - 若增加新的上下文字段,应继续限制输出长度,并避免把失败时的 stderr 直接写入 provider-visible context。
- 若未来实现 Git 状态自动刷新,需要额外的调度、缓存或事件源;这些能力不在本次读取的源码中,不能仅通过现有快照函数推导出来。