资讯动态

如何在 GitButler 中新增一个 but CLI 命令:从参数定义到命令处理、测试与快照更新

发布时间:2026/9/14 15:19:37 来源:尧图企业网站定制
如何在 GitButler 中新增一个 but CLI 命令从参数定义到命令处理、测试与快照更新【免费下载链接】gitbutlerThe GitButler version control client, backed by Git, powered by Tauri/Rust/Svelte项目地址: https://gitcode.com/GitHub_Trending/gi/gitbutler本文描述在 GitButler 仓库中给butCLI 新增一个子命令的完整流程定义 clap 参数、注册子命令、在分发逻辑中处理命令、实现命令体最后用沙箱测试和 snapbox 快照验证行为。目标读者是要修改crates/but/下 CLI 代码的开发者。仓库内置了两份直接指导这一任务的文档cli-commands skill 给出了新命令必须遵循的三层结构crates/but/AGENTS.md 补充了测试与快照的规范。skill 文档以一个假想的commit3命令为例下文中commit3/Commit3都是这个示例名替换成你的新命令名即可。准备工作在仓库根目录下工作but的源码在 crates/but/测试在crates/but/tests/but/。动手前先读两个参考实现skill 文档指明它们是这一结构在实践中的例子crates/but/src/command/legacy/commit.rs、crates/but/src/command/legacy/move.rs、crates/but/src/command/legacy/squash.rs、crates/but/src/command/legacy/diff.rs。如果你的命令会触碰 graph/workspace/branch/stack/commit 关系、可达性或 ref 放置还应先读crates/WORKSPACE_MODEL.mdAGENTS.md 的要求。第一步在 args 模块中定义参数新命令的参数放在crates/but/src/args/下一个命令一个文件例如crates/but/src/args/commit3.rs。skill 文档给出的结构是use crate::args::atoms::CliIdArg; /// Create a commit. /// /// More details about the command here... #[derive(Debug, clap::Parser)] #[cfg_attr(feature raw-clap-docs, clap(verbatim_doc_comment))] #[deny(missing_docs)] pub struct Platform { /// The message to use for the commit. #[clap(short, long, group commit_message)] pub message: OptionVecString, /// Place the commit on the branch BRANCH. #[clap(short, long, value_name BRANCH, group targeting)] pub branch: OptionOptionCliIdArg, /// One or more changes to commit. pub changes: VecCliIdArg, }参数类型有明确约定决定了用户输入如何被解析凡是引用 Git 对象commit、branch、file、hunk 等的参数使用crate::args::atoms里的类型不要直接用String。其中CliIdArg允许用户写短 ID 或完整限定名。String只用于真正自由的文本输入比如 commit message。Platform结构和它的每个字段都必须有 doc commentSubcommands枚举的变体本身故意不写 doc comment因为 clap 的命令说明取自Platform。用#[clap(group ...)]建立互斥参数组。对语法容易写错的命令可以在Platform旁边定义pub(crate) const ERROR_EXAMPLES并注册到args::error_examples该块会追加在 clap 解析错误之后。skill 文档要求最多 4 行每行是一条按原文可执行的but cmd ... # what it does调用例如该带-m就要带上否则解析通过后会打开编辑器。注意 AGENTS.md 的提醒doc comment 会被but skill reference读取并打印每个命令的首段和 flag 帮助所以第一段就要说清命令做什么如果省略某参数时交互式终端与非交互运行的行为不同两种都要写明。第二步把子命令注册进 Subcommands 枚举在 crates/but/src/args/mod.rs 的Subcommands枚举中加一个变体。skill 文档给出的形式现有命令如Commit在 mod.rs 第 280 行即Commit(commit::Platform),#[cfg(feature legacy)] #[cfg_attr(feature raw-clap-docs, clap(verbatim_doc_comment))] #[clap(hide true, name _commit3)] Commit3(commit3::Platform),第三步在 lib.rs 的分发逻辑中处理命令在 crates/but/src/lib.rs 的match cmd里为你的子命令加一个分支。skill 文档给出的处理模板match cmd { Subcommands::Commit3(commit_args) { use crate::utils::IntermediateChannel; let status_after args.status_after; let mut ctx setup::init_ctx( args, InitCtxOptions { background_sync: BackgroundSync::Enabled { silent: false }, ..Default::default() }, out, )?; out.begin_status_after(status_after); let outcome command::legacy::commit3::commit( mut ctx, IntermediateChannel::new(out), commit_args, ) .emit_metrics(metrics_ctx)?; out.print_cli_output(outcome)?; run_status_after_if_requested(status_after, mut ctx, out); Ok(()) } // all the other commands... }这里有两个硬规则命令函数接收IntermediateChannel不要把OutputChannel直接传给命令实现。最终输出用OutputChannel::print_cli_output打印这样所有受支持的输出格式都被处理如果命令只支持人类可读格式用print_cli_output_human。真实命令的分支可以比模板多做一些收尾工作例如现有的Commit分支lib.rs在打印结果前还记录了 metrics 和冲突通知快照。第四步实现命令体resolve → run 结构命令实现放在crates/but/src/command/legacy/commit3.rs这类文件里。skill 文档要求命令遵循先resolve后run的结构pub fn commit( ctx: mut Context, out: IntermediateChannel_, args: Platform, ) - CliResultCommitOutcome { // get whatever dependencies we need from Context such as // RepoExclusiveGuard, IdMap, RefInfo, etc. // resolve the arguments into a CommitOperation let commit_operation resolve(ctx, args)?; // Run the operation let outcome run(ctx, commit_operation)?; // Return the outcome which will be printed by the caller Ok(outcome) } fn resolve(ctx: mut Context, args: Platform) - CliResultCommitOperation { let Platform { message, branch, changes } args; // ... } fn run(ctx: mut Context, commit_op: CommitOperation) - anyhow::ResultCommitOutcome { match commit_op { // ... } } #[must_use] struct CommitOutcome { new_commit: ObjectId, } impl CliOutputHuman for CommitOutcome { fn on_human(self, out: mut dyn WriteWithUtils, _theme: Theme) - anyhow::Result() { let Self { new_commit } self; writeln!( out, Created commit {}, theme::Commit(new_commit, None), )?; Ok(()) } } impl CliOutput for CommitOutcome { fn on_shell(self, out: mut dyn WriteWithUtils) - anyhow::Result() { let Self { new_commit } self; writeln!(out, {}, new_commit.to_hex_with_len(7))?; Ok(()) } fn on_json(self) - impl serde::Serialize { #[derive(Serialize)] struct Output { commit: HexHash, } let Self { new_commit } self; Output { commit: new_commit.into() } } }各部分的职责划分均来自 skill 文档resolve返回CliResult因为它的任务是拒绝非法用户输入它把 CLI 参数翻译为领域目标并校验输入可以查询仓库状态来消歧或拒绝但不应在 operation 里保留推导数据。run返回anyhow::Result因为它只会遇到内部错误它加载当前仓库状态并计算结果。commit 数、diff、统计、分支详情、当前 tip、workspace 投影这类派生状态都属于run()。run自己不打印最终输出而是返回实现了CliOutput/CliOutputHuman的结果类型由调用方打印。operation 类型里不应出现crate::args::atoms的类型也不应放 diff specsVecDiffSpec放CliId由run用DiffSpecBuilder转换。这样 TUI 等非 CLI 调用方可以直接构造 operation 调用run。结果类型不要实现serde::Serialize在on_json里定义需要的具体结构体避免意外破坏兼容性。复用已有领域类型的Serialize是允许的。打印 commit、branch、change ID 等用theme模块的 newtype如theme::Branch、theme::Commit保证颜色一致。用户输入错误用bad_input(...)可搭配.arg_name()、.arg_value()、.hint()。交互式选择器和提示通过IntermediateChannel::prepare_for_terminal_input拿到的InputOutputChannel创建。用AllowMergedArg和MergedUpstream校验避免修改已合并的 commit 和分支。第五步为命令编写沙箱测试but的 CLI 测试位于crates/but/tests/but/command/下一个命令一个文件例如 commit.rs。AGENTS.md 的测试规范断言优先用env.but(...).assert().success()/failure()配合.stdout_eq(snapbox::str![...])和.stderr_eq(snapbox::str![...])输出中不稳定的片段用[..]或...通配符而不是削弱断言。构造测试环境用沙箱辅助方法不要用std::process::Command::new(git)多行命令序列用env.invoke_bash(...)单条 Git 命令用env.invoke_git(...)。避免env.but(...).output()之后直接断言 stdout/stderr输出检查应留在 snapbox 里测试中用会 panic 的assert!、assert_eq!、assert_ne!而不是anyhow::ensure!。commit.rs 开头就是一个典型的失败路径测试展示了场景初始化、执行命令和快照断言的完整写法#[test] fn rejects_unnamed_segment_as_target() { let env Sandbox::init_scenario_with_target_and_default_settings(one-stack-anonymous-segment); env.setup_metadata([A]); env.file(new.txt, content\n); for command in [ commit -b g0 -m test, commit -A g0 -m test, commit -B g0 -m test, ] { env.but(command) .assert() .failure() .stdout_eq(snapbox::str![]) .stderr_eq(snapbox::str![[r# Error: Cannot operate on anonymous branch g0 Hint: Name it with but reword g0 first! Note that the short ID is likely to change when the branch is named. #]]); } }Sandbox::init_scenario_with_target_and_default_settings(...)的参数是crates/but/tests/fixtures/下已有的 shell 场景名env.file(new.txt, content\n)在沙箱仓库里创建文件。新命令的测试文件放在crates/but/tests/but/command/你的命令.rs并登记进该目录的mod.rs。第六步运行测试并更新快照快照是这套测试的核心。首次运行或修改命令输出后按 AGENTS.md 的说明更新SNAPSHOTSoverwrite cargo test -p but条件允许时把范围限定到具体测试名而不是全量覆盖。更新后必须人工检查生成的快照确认测试仍在测试它声称测试的东西——这是 AGENTS.md 的明确要求。彩色终端输出断言的是 SVG 文件用snapbox::file![snapshots/test-name/invocation.stdout.term.svg]的形式更新方式同样是上面那条SNAPSHOTSoverwrite命令。test-name与invocation分别替换为你测试所在快照目录名和被调用的命令形态。测试通过后再核对两件事but skill reference能打印出新命令的首段说明和 flag 帮助doc comment 经由此处暴露给 agent。按 AGENTS.md 的最后一条要求修改 CLI 命令或工作流后要同步更新crates/but/skill/让打包的 agent skill 与命令行为保持一致。已知限制与排查worktree 锁死锁命令处理器应在操作顶部获取所需 worktree guard并把派生的权限沿调用链传递已有 guard 时优先用*_with_perm(...)这类取权限的辅助函数不要在持有 guard 期间再获取另一个共享或独占 guard。怀疑死锁时用 debug 构建并设置BUT_WS_LOCK_DEBUG1它会让重复获取锁直接 panic 而不是无限阻塞配合 backtrace 定位嵌套获取BUT_WS_LOCK_DEBUG1 RUST_BACKTRACE1 cargo run -p but -- -C repo command命令中repo是目标仓库路径对应but的-C参数command是触发问题的命令二者按实际替换。找到嵌套点之后把已有的权限传到该调用点或改用取权限的辅助函数。文档中命令示例的准确性ERROR_EXAMPLES里的每条调用必须按原文可执行修改命令参数或行为时要同步检查skill 文档和 AGENTS.md 都把它列为需要保持准确的项。小结一条but子命令的落点固定为四处crates/but/src/args/cmd.rs定义Platform参数、crates/but/src/args/mod.rs注册变体、crates/but/src/lib.rs分发并初始化 context、crates/but/src/command/legacy/cmd.rs按 resolve/run 结构实现。测试与快照放在crates/but/tests/but/command/用SNAPSHOTSoverwrite cargo test -p but更新并逐一人工核对最后同步crates/but/skill/。以上每一步的结构与约束都出自 cli-commands skill 与 crates/but/AGENTS.md可直接对照commit.rs等现有实现逐条核验。【免费下载链接】gitbutlerThe GitButler version control client, backed by Git, powered by Tauri/Rust/Svelte项目地址: https://gitcode.com/GitHub_Trending/gi/gitbutler创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价