name: jj-split-changeset description: 将jj(Jujutsu)变更集拆分为更小、更专注的变更集。当需要拆分大型变更集、分离提交、跨版本重新组织更改或从单个变更集创建堆叠PR时使用。涵盖基于安全复制的工作流、文件路径和代码块级别的拆分,无需交互式命令。
拆分JJ变更集
将变更集$ARGUMENTS拆分为更小、更专注的单元——安全、高效,并在适当时刻让用户参与。
如果$ARGUMENTS为空,请在继续前询问用户要拆分的revset。
目录
- 核心安全原则:先复制
- 工作流概述
- 检查变更集
- 与用户规划
- 4.1 如何分组更改
- 4.2 每个变更集应获得的描述
- 4.3 如何验证每个变更集
- 复制变更集
- 拆分变更集
- 设置描述
- 验证拆分
- 重新基于依赖项(如果需要)
- 清理
- 关键命令参考
- 何时提示用户
- 限制
1. 核心安全原则:先复制
永远不要直接编辑原始变更集。 总是先复制它,然后在副本上工作。这为您提供了免费的撤销操作——原始变更集在拆分确认正确前保持不变。
# 总是从这里开始
jj duplicate <revset> # 创建相同的副本;打印新变更ID
只有在拆分完成并验证后,才应放弃原始变更集:
jj abandon <original-revset>
此原则适用于以下每个步骤。
2. 工作流概述
- 检查 — 了解变更集内容
- 与用户规划 — 就分组、描述和验证标准达成一致
- 复制 — 创建安全的工作副本
- 拆分 — 基于文件路径或代码块级别
- 描述 — 为每个结果变更集设置有意义的描述
- 验证 — 确认没有更改丢失,然后运行用户定义的验证
- 重新基于依赖项 — 询问用户是否有任何内容需要重新基于
- 清理 — 放弃原始变更集
3. 检查变更集
在进行任何操作前,了解您正在处理的内容。
# 显示完整差异,包括文件名和统计信息
jj diff -r <revset> --stat
jj diff -r <revset>
# 显示变更集描述和父级
jj log -r <revset>
为用户总结:
- 更改了多少文件
- 哪些文件在逻辑上相关
- 是否有清晰的分组(例如,“重构” vs “功能” vs “测试”)
4. 与用户规划
在继续前,总是询问用户三件事。
4.1. 如何分组更改
呈现文件列表和您的建议分组,但让用户决定。
示例提示:
此变更集涉及8个文件。我看到三个逻辑组:
- 重构:
src/foo.rs,src/bar.rs(签名更改)- 功能:
src/new_thing.rs,src/lib.rs(新功能)- 测试:
tests/test_new_thing.rs,tests/test_foo.rs这个分组看起来对吗?您想以不同方式拆分吗?
单个文件内的混合代码块: 如果文件有属于不同逻辑组的更改,请向用户标记此情况。代理可以处理代码块级别的拆分,无需交互性 — 见 6.2 代码块级别拆分。呈现文件中的代码块,并询问用户哪些代码块属于哪个组。
4.2. 每个变更集应获得的描述
询问提交消息。基于分组建议默认值,但让用户确认或覆盖。
4.3. 如何验证每个变更集
询问用户如何验证每个拆分变更集是否正确超出差异统计审查。示例:
我应该如何验证每个拆分变更集?一些选项:
- 构建检查:每次拆分后运行
cargo build/cargo check- 测试套件:
cargo test(所有测试,或特定子集?)- 代码检查/格式化:
cargo clippy,cargo fmt --check- 仅差异审查:只显示差异,我会目视检查
您可以为每组指定不同的验证(例如,“为功能变更集运行测试,仅为重构进行差异审查”)。
存储验证计划 — 您将在步骤8.3中执行它。
5. 复制变更集
jj duplicate <revset>
# 注意输出中的新变更ID — 这是您的工作副本
所有后续操作都针对副本,而非原始变更集。
6. 拆分变更集
不要使用jj split -i(交互模式) — 它打开编辑器,这在代理上下文中不起作用。使用文件路径拆分或下面的手动重建方法。
6.1. 文件路径拆分(当每个文件属于一个组时)
jj split -r <rev> <paths...>将变更集分成两个:
- 第一个变更集:仅包含指定路径的更改
- 第二个变更集:包含其余所有内容
要分成两个以上组,对剩余部分重复运行jj split。
jj duplicate <original>
# 假设这产生变更ID:abc
# 第一次拆分:提取重构文件
jj split -r abc src/foo.rs src/bar.rs
# 输出告诉您两个新变更ID。
# “剩余”变更集(除foo+bar外的所有内容) — 假设是def。
# 第二次拆分:从剩余部分提取功能文件
jj split -r def src/new_thing.rs src/lib.rs
# 现在您有三个变更集:重构、功能、测试
6.2. 代码块级别拆分(当文件有混合更改时)
当单个文件包含属于不同逻辑组的代码块时,您不能使用jj split -r <rev> <path>,因为它会移动整个文件。相反,通过创建空变更集并将所需文件内容写入它们来手动构建每个变更集。
策略: 对于每组,在父级上创建一个新的空变更集,然后填充它 — 使用jj restore --from <duplicate>用于包含整个文件,以及直接文件写入用于部分文件包含。
jj duplicate <original>
# 假设这产生变更ID:dup
# 识别副本的父级
jj log -r 'dup-' --no-graph -T 'change_id'
# 假设父级是:parent
# --- 组1:重构(整个文件src/bar.rs + src/foo.rs中的一些代码块) ---
# 在父级上创建一个空变更集
jj new <parent>
# 现在工作副本是一个新的空变更集 — 假设是g1
# 恢复完全属于此组的整个文件
jj restore --from <dup> src/bar.rs
# 对于src/foo.rs,只有一些代码块属于这里。
# 1. 读取父级状态下的文件(“之前”状态)
# 2. 读取副本的完整差异以了解所有代码块
# 3. 仅应用所需的代码块以产生正确的文件内容
# 4. 直接写入结果
jj diff -r <dup> src/foo.rs # 检查代码块,决定哪些属于组1
# 写入仅应用组1更改的文件:
cat > src/foo.rs << 'EOF'
... 仅应用重构代码块的文件内容 ...
EOF
# --- 组2:功能(src/foo.rs的剩余部分 + src/new_thing.rs + src/lib.rs) ---
jj new <g1>
# 新的空变更集 — 假设是g2
# 恢复整个文件
jj restore --from <dup> src/new_thing.rs src/lib.rs
# 对于src/foo.rs,应用剩余代码块(不在组1中的那些)。
# 起始状态是g1的foo.rs版本(已包含重构代码块)。
# 写入包含重构和功能代码块的最终版本:
cat > src/foo.rs << 'EOF'
... 在g1基础上应用功能代码块的文件内容 ...
EOF
# --- 组3:测试(剩余的整个文件) ---
jj new <g2>
jj restore --from <dup> tests/test_new_thing.rs tests/test_foo.rs
如何产生部分文件内容:
- 运行
jj diff -r <dup> <path>查看文件的所有代码块。 - 读取父级状态下的文件:
jj cat -r <parent> <path>。 - 确定哪些代码块属于当前组(来自步骤4.1中与用户商定的计划)。
- 仅将这些代码块应用到父级状态内容以产生所需文件。
- 将结果写入工作副本。
这比文件路径拆分更繁琐,但它提供了完整的代码块级别控制,无需任何交互式命令。
6.3. 顺序重要
考虑组的依赖顺序。如果功能更改依赖于重构,首先提取重构,以便它成为功能变更集的父级。jj split和手动方法都创建父级→子级链。
7. 设置描述
应用步骤4.2中商定的描述:
jj describe -r <revset1> -m "重构:更新foo和bar签名"
jj describe -r <revset2> -m "功能:添加new_thing实现"
jj describe -r <revset3> -m "测试:添加new_thing和更新foo的测试"
8. 验证拆分
验证有两部分:完整性(无更改丢失)和正确性(每个变更集单独有效)。
8.1. 验证完整性 — 无更改丢失
使用jj interdiff确认拆分结果与原匹配:
jj interdiff --from <original-revset> --to <last-split-revset>
如果拆分正确,这产生无输出(空差异)。任何输出意味着更改丢失或重复。
如果jj interdiff在您的版本中不可用,回退到比较原始差异:
diff <(jj diff -r <original-revset>) <(jj diff -r <first-split> && jj diff -r <second-split> && jj diff -r <third-split>)
8.2. 验证单个变更集 — 统计审查
向用户显示每个变更集的统计摘要:
jj diff -r <revset1> --stat
jj diff -r <revset2> --stat
jj diff -r <revset3> --stat
8.3. 运行用户定义的验证
执行步骤4.3中的验证计划。对于每个变更集,签出该修订版的状态并运行商定的检查:
# 示例:验证重构变更集可构建
jj new <revset1>
cargo check
# ... 然后验证下一个变更集,等等。
如果任何验证失败,向用户报告失败并询问如何继续 — 不要自动放弃原始变更集或继续。
呈现所有验证结果的摘要,并在继续前询问用户确认。
9. 重新基于依赖项(如果需要)
只有在所有验证通过且用户确认后,检查是否有其他变更集依赖于原始变更集:
jj log -r '<original-revset>+'
如果有依赖项,询问用户它们应重新基于哪个拆分变更集:
以下变更集是原始变更集的子级:
xyz(“添加基准测试”)它们应重新基于哪个拆分变更集?
<revset1>— “重构:更新foo和bar签名”<revset2>— “功能:添加new_thing实现”<revset3>— “测试:添加new_thing和更新foo的测试”
然后按照指示重新基于:
jj rebase -s <dependent> -o <new-parent>
10. 清理
一旦所有内容都验证且依赖项处理完毕:
jj abandon <original-revset>
11. 关键命令参考
| 命令 | 目的 |
|---|---|
jj duplicate <revset> |
在任何破坏性操作前创建安全副本 |
jj split -r <revset> <paths...> |
按文件路径拆分(指定路径 → 第一个变更集,其余 → 第二个) |
jj restore --from <src> [paths...] |
从另一个变更集复制文件状态到当前工作变更集 |
jj interdiff --from <a> --to <b> |
显示两个变更集之间的差异(空 = 相同) |
jj cat -r <revset> <path> |
打印给定修订版的文件内容 |
jj describe -r <revset> -m "..." |
设置变更集描述 |
jj abandon <revset> |
放弃变更集(仅在拆分验证后) |
jj rebase -s <src> -o <dest> |
将变更集及其后代重新基于新父级 |
jj diff -r <revset> --stat |
显示变更集的更改摘要 |
jj diff -r <revset> |
显示变更集的完整差异 |
jj log -r <revset> |
显示变更集元数据 |
jj new <revset> |
作为子级创建新的空变更集 |
12. 何时提示用户
总是在以下情况前询问:
不要询问,直接执行:
jj duplicate— 总是安全的jj diff --stat/jj diff— 只读检查jj log— 只读检查jj interdiff— 只读验证- 运行商定的验证命令(8.3)
13. 限制
- 无交互式命令。 永不使用
jj split -i或任何打开编辑器的命令。对整文件拆分使用基于文件路径的jj split,对代码块级别拆分使用手动jj new+jj restore+ 文件写入方法。 - 代码块级别拆分需要谨慎。 手动应用代码块时,验证结果文件内容正确。这里的错误比错误文件分组更难发现。8.1中的完整性检查将捕获丢失或重复的更改。