Repository Wiki
zai-org/ZCode

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 相关读取被拆成几个层次:

  1. git-snapshot.ts 面向环境上下文,执行一组低成本 Git 查询并返回 Partial<EnvInfo>。它适合把仓库状态注入运行时上下文。
  2. workflow-git-world-read.ts 的前半部分是纯 argv 构造与解析契约。它强调“只读是构造出来的”,而不是先构造任意命令、再依赖运行时检查阻止写操作。
  3. tui-workspace-git.ts 面向 TUI 工作区,只读取当前分支;它使用更短的超时和更小的输出上限,并将 detached HEAD 或任何失败转换为 undefined。

这些实现共同遵循两个关键约束:Git 命令始终通过参数数组执行,而不是拼接 shell 字符串;工作区相对路径必须被约束在当前工作区范围内。这样做的目的不是提供 Git 写入能力,而是让状态展示、上下文注入和只读 world-read 的边界在代码结构上可验证。

Architecture

Loading diagram...

架构中的 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 或直接中断。

typescript
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 的已跟踪历史差异。

typescript
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。成功时只取第一行并去除空白。

typescript
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 timeoutnumber3000 msgit-snapshot.ts 中每次 execFile 的超时
Git snapshot max buffernumber1048576 bytesGit snapshot 子进程 stdout/stderr 缓冲上限
Git status context limitnumber2000 字符注入上下文的状态文本上限
Recent commits limitnumber5最近提交数量上限
TUI branch timeoutnumber750 msTUI 分支读取超时
TUI branch output limitnumber512 bytesTUI 分支读取 stdout 上限

源码没有显示环境变量或配置文件覆盖这些常量;因此不应把它们描述为用户可配置项。

Core Flow

Loading diagram...

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 状态自动刷新,需要额外的调度、缓存或事件源;这些能力不在本次读取的源码中,不能仅通过现有快照函数推导出来。

Sources

(4 files)
apps/zcode-cli/packages/adapters/src/context
apps/zcode-cli/packages/bootstrap/src/app
apps/zcode-cli/packages/cli/src