ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Jujutsu `jj run` 设计解析:跨修订并行运行命令、自动改写提交与失败处理策略

Jujutsu `jj run` 设计解析:跨修订并行运行命令、自动改写提交与失败处理策略 Jujutsujj run设计解析跨修订并行运行命令、自动改写提交与失败处理策略【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jjjj run是 Jujutsu 中用于在多个修订revision上批量执行用户命令脚本、linter、formatter、构建系统等的命令它把每个目标修订检出到隔离的临时工作副本中运行子进程再把子进程产生的改动自动改写入对应提交。本文以仓库中的设计文档 docs/design/run.md 为骨架结合jj run的实际实现 cli/src/commands/run.rs 与测试 cli/tests/test_run_command.rs完整讲解该命令的设计动机、目标与非目标、工作副本池机制、提交改写模型、并行与失败策略、全部命令选项以及设计提案与最终实现的差异。读完后你将理解jj run的设计取舍掌握在本地仓库中用它批量跑测试、格式化、lint 与构建的完整方法并能读懂其底层工作副本管理、transform_descendants重写与进程调度实现。设计背景与前身Preface Contextjj run的设计文档Initial Version, 10.12.2022目标是明确jj run的正确行为规范并作为后续jj test、jj fix、jj format等专用命令的基础设施。在设计之初作者调研了其他 DVCS 中的既有实现前身实现所属项目与本提案的关系git testgit-branchless与本提案最接近直接启发了为每个并行命令创建临时工作副本的做法hg runGoogle 内部 Mercurial 扩展功能类似但依赖 CitCClients in the Cloud虚拟文件系统做惰性应用hg fixGoogle 开源 Mercurial 扩展更专门化在没有完整工作目录上下文的情况下重写文件内容git rebase -xGit在 rebase 过程中机会式运行命令git bisect runGit运行命令以定位引入 bug 的提交需求来源最初的需求来自一次 GitHub 讨论关于 pre-commit 集成而在关于 git hook 模型的 Discord 讨论中社区达成共识——不要重蹈 git hook 的覆辙。另一个关键约束是目前 Jujutsu 的开源后端Git、Simple都没有支持虚拟文件系统的工作副本因此无法像 Google 内部hg run那样懒应用命令到受影响的文件在实现基于虚拟文件系统的工作副本之前jj run只能在常规本地磁盘工作副本中运行命令。目标与非目标目标Goals命令应能应用于任意修订无论已发布published还是未发布unpublished。能并行运行实际命令同时保持良好的控制台输出。命令能作用于任意提交包括工作副本提交本身或其他任何提交。存在某种方式指示硬失败hard failure。构建足够的基础设施为将来的jj test、jj fix、jj format铺路。主要目标是足够好good enough功能可随未来迭代扩展。非目标Non-Goals不为jj run堆叠jj test/jj format/jj fix的用例——那是它们各自的职责。命令不应太聪明对工作流做过多的假设只会让用户困惑。不做输出结果的智能缓存因为用户输入的命令不可预测。不做细粒度的用户可见配置避免不必要的复杂度。不在jj run中塞入fix子命令那会过度压缩设计空间。这些原则在后面设计里得到了贯彻实现保持简单直接把智能留给未来的专用命令。典型使用场景Use-Cases设计文档给出了三类典型场景Lint 与格式化jj run pre-commit run -r $revset jj run cargo clippy -r $revset jj run cargo nightly fmt跨仓库本地与远端的大规模改动jj run sed /some/test/ -r mine() ~remote_bookmarks(exact:origin) jj run $rewrite-tool -r $revset构建系统jj run bazel build //some/target:somewhere jj run ninja check-lld设计文档同时指出部分用例应获得专用命令以便进一步优化例如jj format在一个修订的某文件子集上运行一串 formatter和jj fix在修订的某文件子集上运行rustfmt --fix或cargo clippy --fix。当前仓库已落地了与设计一脉相承的fix命令族见 cli/src/commands/fix.rs 及其配置说明 docs/config.md这正是设计文档中jj run构建基础设施目标的体现。核心设计.jj/目录下的临时工作副本池设计提案Base Design设计文档提出的基础方案是所有工作都在仓库的.jj/目录内完成从而对用户隐藏全部复杂度同时保留用户当前工作区不动。要点如下借鉴 git-branchlessgit test的做法为每个并行命令创建一个临时工作副本。工作副本在多次jj run调用之间复用如果待处理的提交数多于并行任务数也在单次调用内部复用。保留临时目录中的被忽略文件ignored files这能让增量构建受益例如让 cargo 复用其target/目录但代价是运行结果可能变得不那么可复现。因此设计一个 flag 用于从临时工作副本中移除被忽略文件。保留被忽略文件还带来磁盘占用问题cargo 的target/经常占用数十 GB同一个清理 flag 可同时应对此外可能还需要一个运行命令之后清理临时工作副本的 flag。早期版本直接用TreeState管理临时工作副本意味着在临时工作副本内部再运行jj将不可用后续可扩展为完整的Workspace。为防止临时工作副本中的操作影响主仓库可使用独立的 OpHeadsStore。实现印证WorkspacePool实现把上述设计落到了 cli/src/commands/run.rs 的WorkspacePool结构run.rs#L159-L350注释明确写道工作区池位于.jj/run/default/下每个槽位是.jj/run/default/N/内含working_copy/与state/子目录锁文件为兄弟文件.jj/run/default/N.lockrun.rs#L154-L158。工作区在jj run调用之间持久保留使构建产物可复用获取槽位时取第一个空闲者因此多个并发的jj run进程会协作共享这个池。槽位获取使用文件锁 指数退避重试10ms 起步、250ms 封顶见 run.rs#L196-L204持锁期间RunWorkspace持有的FileLock保证独占丢弃即释放run.rs#L111-L120。复用工作区时先加载持久化的 tree statecheck_out会只 diff 变更文件persist()在任务结束后把快照后的 tree state 写回磁盘供下次获取时做增量对比run.rs#L122-L129。崩溃恢复检出前先删除磁盘上的tree_state作为脏标记若上次任务中途崩溃下次获取会看到文件缺失而整体清空该槽位避免信任不一致的状态run.rs#L218-L229。关于临时工作副本中能否运行jj当前实现确实只使用TreeState见 lib/src/local_working_copy.rs没有为槽位建立完整 Workspace与设计文档早期版本直接用 Treestate的规划一致。修改工作副本squash、reparent 与忽略改动设计文档提出子进程在临时工作副本中运行不会干扰用户自己的工作副本因此jj run运行期间用户可以继续工作。子进程被允许通过更新自己分配的工作副本对仓库做改动具体语义以只对提交 A、B 运行B 的父是 A为例在 A 之上产生的任何改动会被squash压合进 A形成 A同理B 之上的改动被压合成 B。之后有两种选择对 B 相对 A 做普通 rebase或者直接把 B 的父指针更新为 Areparent。前者在子进程只基于父提交做部分树更新时更合适。此外可能还需要一个忽略子进程工作副本中一切改动的选项。实现印证三种提交改写模式实际实现提供了对应三种模式的选项run.rs#L567-L664 的RunArgs实现选项对应设计语义默认无选项把命令引入的 diffnew_tree − old_tree合并传播到 rebase 后的树上后代随祖先改写而 rebase--restore-descendants保留后代内容不变对应设计中的 reparent/只更新父指针--ignore-changes命令照跑但不重写任何提交改动全部丢弃具体重写逻辑在cmd_run末尾的transform_descendants中run.rs#L855-L898默认模式下用三方合并把(new_tree, command result)、(old_tree, original commit)、(rebased_tree, rebased)合并从而把命令产生的差异嫁接到已 rebase 的树上--restore_descendants模式下则直接set_tree(new_tree)忽略祖先改写对内容的影响对不在 run 集合内的后代parents_changed()时分别走reparent()restore 模式或rebase()默认模式。结束时输出统计信息如Rewrote N commits.与Rebased N descendant commits.run.rs#L899-L909。测试 cli/tests/test_run_command.rs 中的test_run_noop验证了命令未改动任何 tracked 文件时提交不被重写的路径。修改仓库独立 OpHeadsStore 与父指针校验设计文档指出一旦通过独立的 OpHeadsStore给予子进程仓库的 fork 访问权子进程就能在自己的 fork 中创建新操作operation。此时若用户运行jj run -r foo而子进程 checkout 了另一个提交行为是不明确的——合理的做法是在子进程返回后校验工作副本提交的父指针未变子进程创建的任何操作将被忽略。底层相关的操作存储实现在 lib/src/op_heads_store.rs。重写修订与不可变提交与其他命令一致jj run拒绝重写 public/immutable 提交对私有/未发布修订通过命令选项执行 amend 或 reparent。实现印证cmd_run在改写前调用check_rewritable除非传了--ignore-changesrun.rs#L722-L726。测试test_run_on_immutable展示了错误输出Error: The root commit 000000000000 is immutable即对all()包含 root运行时会因不可变提交直接失败见 cli/tests/test_run_command.rs。执行顺序与并行度设计文档认为按拓扑顺序执行很有用——例如构建系统这类成本与增量变化成正比的命令拓扑序也是在首个失败处停止这类功能有意义的前提对一串提交跑测试时按拓扑/时间顺序前进、遇首个失败即停因为后续很可能同样失败。并行调度时可以让每个执行槽尽量沿用工作副本以减少增量变化但如果需要让所有提交并发运行也应可行。实现印证jobs 解析与并发调度并行度的解析遵循优先级--jobs命令行参数 run.jobs配置 默认 1resolve_jobsrun.rs#L667-L693。配置文件写法见 docs/config.md#run[run] jobs 8run.jobs必须是正整数命令行可用-j/--jobs覆盖例如jj run -j 4 -- cargo fmt调度实现使用 Tokio 的JoinSet保持最多jobs个任务在飞、按提交顺序启动run_innerrun.rs#L386-L432并发写控制台时每个子进程的 stdout/stderr 被先完整缓冲、进程结束后原子输出避免多任务输出交错run.rs#L506-L512。--passthrough则直接把 stdout/stderr 接到终端以支持 TTY 程序但此时强制只允许 1 个 jobrun.rs#L730-L735。另外默认目标修订集由配置revsets.run决定内置值为reachable(, mutable())见 cli/src/config/revsets.toml即从可达的可变提交未指定-r时按此集合执行run.rs#L706-L712。失败处理策略Dealing with failure设计文档定义了三种由 UI 选项暴露的策略用于定制冲突处理策略行为Continue某个子进程失败后继续处理子修订退出时向用户报告失败的修订Stop发出致命失败信号取消尚未开始的已调度工作但让已启动的子进程跑完向用户显示子进程产生的错误Fatal立即停止处理并杀死所有正在运行的进程告知用户未能将该命令应用到特定修订设计同时承诺只要任一子进程失败受影响的提交保持原状不落地部分格式化等半成品状态以提供更好的用户体验。实现印证与差异实际实现以默认在首个失败处停止 --ignore-errors继续的方式落地了该策略默认相当于 Stoprun_inner中一旦有任务失败且未设ignore_errors会command_futures.shutdown()等待已启动进程退出避免僵尸进程随后消费者循环返回包含失败修订摘要的CommandErrorrun.rs#L414-L429、run.rs#L823-L832。--ignore-errors相当于 Continue继续处理剩余修订失败命令的改动不会保存成功命令的改动在最后原子应用失败命令的退出码不会影响jj run自身的退出码run.rs#L654-L663。失败不改写提交rewrite_commit在命令失败时返回new_tree: None只有成功任务的新树才会进入rewritten_commitsrun.rs#L544-L554同时会清掉命令留下的非忽略未跟踪文件避免污染下一次check_outrun.rs#L524-L537。对应测试覆盖了test_run_stops_after_first_failure、test_run_failure_rewrites_nothing、test_run_ignore_errors_rewrites_successes、test_run_ignore_errors_all_fail、test_run_recovers_after_failure等路径见 cli/tests/test_run_command.rs。需要注意的是设计提案中的--error-strategycontinue|stop|fatal单一 flag 在实现中演化成了默认 stop --ignore-errors的组合。资源约束Resource constraints设计文档提出约束执行以防止资源耗尽相关资源包括机器上的 CPU 与内存jj run可提供简单缓解措施例如默认将并行度限制为CPU 核数或按可用内存 / 每次调用内存估算来限制并行度。jj 未知的外部资源例如并行命令可能希望限制到某个服务器的总连接数。设计倾向是把这类约束推迟给被调用命令自身的实现处理而不是让 jj 去感知和传递这些信息。命令选项全览设计文档强调任何 jj 命令的基础形态都应可用默认情况下jj run作用于当前工作副本。设计提案列出的选项如下设计提案选项说明--command第一个参数的显式名称-x兼容 git可能别名其他命令-j,--jobs并行度-k,--keep-going失败时继续可能别名其他命令--show显示受影响修订的 diff--dry-run不真正执行记录所有拟执行的文件与参数--rebase在所有父级上 rebase 产生的 diff可能别名其他命令--reparent把受影响修订的父改为新变更可能别名其他命令--clean移除现有工作区与被忽略文件--readonly跨多次 run 调用忽略改动--error-strategycontinue\|stop\|fatal见上文失败策略实现中的实际选项当前实现的RunArgsrun.rs#L587-L664为选项说明COMMAND位置参数ARGS要运行的命令及其参数用--分隔符可传递以-开头的参数如jj run --revisions... -- cargo build --release-r,--revision别名--revisions要处理的修订集revset可重复指定-x隐藏无操作选项仅为匹配git rebase -x的界面-j,--jobs并行进程数覆盖run.jobs配置默认 1--root在每个提交的工作副本根目录运行命令而非从调用jj run的子目录运行--clean运行命令前删除各工作副本使每个提交从全新检出的树开始默认复用工作副本以保留构建产物--restore-descendantsrebase 后代时保留其内容而非保留 diff--passthrough把 stdout/stderr 直接接到终端支持 TTY 行为如彩色输出、进度条stdin 不继承只允许 1 个 job--ignore-changes检出并运行命令但不重写任何提交适合只读检查测试、linter允许作用于 immutable 提交而无需--ignore-immutable与--restore-descendants互斥--ignore-errors命令失败时继续处理剩余修订见失败策略子进程环境变量实现为每个子进程设置三个环境变量run.rs#L486-L494子进程与脚本可用它们感知当前修订JJ_CHANGE_ID当前提交的 change idreverse hexJJ_COMMIT_ID当前提交的 commit idhexJJ_WORKSPACE_ROOT本次运行的工作副本根目录即临时工作副本测试test_run_sets_env_vars验证了子进程把JJ_CHANGE_ID与JJ_COMMIT_ID写入文件后jj run会把它们随提交一起落地见 cli/tests/test_run_command.rs#L160-L234。与 git rebase -x 的兼容示例沿用实现 doc 注释中的示例# 在本地工作上运行 pre-commit jj run -j 4 -- pre-commit run .github/pre-commit.yaml另外注意实现里-x仅作隐藏的 no-op 以匹配git rebase -x的界面run.rs#L605-L607与设计提案一致。与其他命令的集成设计文档明确了各命令与jj run的协同方式命令处理方式jj log无需特殊处理jj diff无需特殊处理jj st现阶段重新打印jj run的最终输出jj op log无需特殊处理但有待在 issue #963 中进一步讨论jj undo/jj op revert无需特殊处理由于jj run的产物是按提交改写而 Jujutsu 的操作日志天然记录这类重写因此jj undo、jj op revert等即可按通用语义回滚jj run造成的修改。开放问题Open Points设计文档在成稿时留下了三个开放问题该命令是否应与工作副本后端绑定working copy backend specific如何管理命令产生的进程配置选项应是用户级还是仓库级这些问题的部分答案已在实现中给出工作副本复用逻辑放在WorkspacePool中、进程由 Tokio 管理、run.jobs与revsets.run成为用户级配置项但进程管理细节与后端绑定问题仍属可演化的设计空间。未来可能性Future possibilities设计文档列举了若干未来方向在内存中重写文件这是一个不错的优化避免反复物化工作副本。暴露部分内部状态以实现更精确的资源约束。与虚拟文件系统的集成选项使其可以缓存所需的工作副本。一个Jujutsu 全局的缓存工作副本概念因为物化工作副本可能很昂贵。定制失败消息这对机器人bots可能有用类似 Bazel 的select(..., message arch not supported for $project)。让jj run异步化spawn 一个主进程后直接返回用户并增量更新jj st的输出。实现中也能看到相关伏笔例如rewrite_commit中将新树序列化到/output/{id-tree}以便缓存查找及以 trait 抽象执行器以对接 Bazel RE 协议的 TODO 注释run.rs#L476-L483、run.rs#L550-L551。从设计到实现的代码导航如果想深入阅读jj run的落地情况以下仓库路径可以直接跳转设计文档原始版本docs/design/run.md网站版位于 web/docs/src/content/docs/design/run.md命令实现RunArgs、WorkspacePool、run_inner、rewrite_commit、cmd_runcli/src/commands/run.rs命令注册与分发cli/src/commands/mod.rs#L216集成测试覆盖环境变量、失败策略、子目录跳过、root flag、默认 revset 等cli/tests/test_run_command.rsrun.jobs配置说明docs/config.md#run默认修订集revsets.run reachable(, mutable())cli/src/config/revsets.toml底层依赖组件TreeState 位于 lib/src/local_working_copy.rs操作存储 lib/src/op_heads_store.rs工作区抽象 lib/src/workspace.rs总而言之jj run的设计文档确立了一条简单、不过度聪明、可并行、失败可定制的路线用.jj/下的临时工作副本池隔离副作用、用 squash/reparent 模型传播子进程改动、用三种失败策略把控制权交还用户同时为jj test、jj fix、jj format预留了基础设施。而当前实现则忠实地把这份设计落到了WorkspacePool、Tokio 任务调度与transform_descendants重写管线上二者对照阅读是理解 Jujutsu 如何在命令执行与提交改写之间建立桥梁的最佳途径。【免费下载链接】jjA Git-compatible VCS that is both simple and powerful项目地址: https://gitcode.com/GitHub_Trending/jj/jj创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表