mirror of
https://github.com/jj-vcs/jj.git
synced 2026-07-03 14:02:54 +08:00
run: Add --restore-descendants option
This commit is contained in:
@@ -548,8 +548,9 @@ async fn rewrite_commit(
|
||||
/// Run a command across a set of revisions.
|
||||
///
|
||||
/// Checks out each revision in an isolated working copy, runs the command, then
|
||||
/// amends the revision with the resulting working copy. Descendants are rebased
|
||||
/// on top of the amended revisions.
|
||||
/// amends the revision with the resulting working copy. By default, descendants
|
||||
/// are rebased on top of the amended revisions, propagating the diff. Use
|
||||
/// `--restore-descendants` to keep descendants' content unchanged instead.
|
||||
///
|
||||
/// The command is executed with the following environment variables set:
|
||||
///
|
||||
@@ -604,6 +605,10 @@ pub struct RunArgs {
|
||||
/// freshly checked-out tree.
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
|
||||
/// Preserve the content (not the diff) when rebasing descendants
|
||||
#[arg(long)]
|
||||
restore_descendants: bool,
|
||||
}
|
||||
|
||||
/// Precedence: `--jobs`, `run.jobs` config, 1.
|
||||
@@ -780,42 +785,64 @@ pub async fn cmd_run(
|
||||
}
|
||||
|
||||
// The command did something, so rewrite the commits.
|
||||
let restore_descendants = args.restore_descendants;
|
||||
let mut count: u32 = 0;
|
||||
// TODO: handle the `--reparent` case here.
|
||||
let mut num_reparented: u32 = 0;
|
||||
tx.repo_mut()
|
||||
.transform_descendants(
|
||||
resolved_commits.iter().ids().cloned().collect_vec(),
|
||||
async |rewriter| {
|
||||
let old_id = rewriter.old_commit().id().clone();
|
||||
let builder = rewriter.rebase().await?;
|
||||
// Only rewrite the tree if the command changed it. Descendants
|
||||
// that weren't part of the input set still need to be rebased
|
||||
// but keep their original tree.
|
||||
if let Some((old_tree, new_tree)) = rewritten_commits.get(&old_id) {
|
||||
// Apply only the diff the command introduced (new_tree -
|
||||
// old_tree) on top of the rebased tree. This propagates
|
||||
// changes into descendants via the normal rebase merge
|
||||
// rather than being replaced.
|
||||
let rebased_tree = builder.tree();
|
||||
let merged = MergedTree::merge(Merge::from_vec(vec![
|
||||
(
|
||||
MergedTree::resolved(store.clone(), new_tree.id().clone()),
|
||||
"command result".to_owned(),
|
||||
),
|
||||
(old_tree.clone(), "original commit".to_owned()),
|
||||
(rebased_tree, "rebased".to_owned()),
|
||||
]))
|
||||
.await?;
|
||||
count += 1;
|
||||
builder.set_tree(merged).write().await?;
|
||||
} else {
|
||||
builder.write().await?;
|
||||
match (rewritten_commits.get(&old_id), restore_descendants) {
|
||||
(Some((_, new_tree)), true) => {
|
||||
let builder = rewriter.rebase().await?;
|
||||
count += 1;
|
||||
// Use the command result on top of the commit's
|
||||
// original tree, ignoring rewrites of its ancestors.
|
||||
builder
|
||||
.set_tree(MergedTree::resolved(store.clone(), new_tree.id().clone()))
|
||||
.write()
|
||||
.await?;
|
||||
}
|
||||
(Some((old_tree, new_tree)), false) => {
|
||||
let builder = rewriter.rebase().await?;
|
||||
count += 1;
|
||||
// Apply the diff the command introduced (new_tree -
|
||||
// old_tree) on top of the rebased tree, propagating
|
||||
// ancestor rewrites via the normal rebase merge.
|
||||
let rebased_tree = builder.tree();
|
||||
let merged = MergedTree::merge(Merge::from_vec(vec![
|
||||
(
|
||||
MergedTree::resolved(store.clone(), new_tree.id().clone()),
|
||||
"command result".to_owned(),
|
||||
),
|
||||
(old_tree.clone(), "original commit".to_owned()),
|
||||
(rebased_tree, "rebased".to_owned()),
|
||||
]))
|
||||
.await?;
|
||||
builder.set_tree(merged).write().await?;
|
||||
}
|
||||
(None, true) => {
|
||||
// Descendant outside the run set — keep its content.
|
||||
rewriter.reparent().write().await?;
|
||||
num_reparented += 1;
|
||||
}
|
||||
(None, false) => {
|
||||
// Default: propagate the diff into descendants.
|
||||
rewriter.rebase().await?.write().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
writeln!(ui.stderr(), "Rewrote {count} commits")?;
|
||||
if restore_descendants && num_reparented > 0 {
|
||||
writeln!(
|
||||
ui.stderr(),
|
||||
"Rebased {num_reparented} descendant commits (while preserving their content)"
|
||||
)?;
|
||||
}
|
||||
tx.finish(ui, format!("run: rewrite {count} commits"))
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -2968,8 +2968,9 @@ Show the current workspace root directory (shortcut for `jj workspace root`)
|
||||
Run a command across a set of revisions.
|
||||
|
||||
Checks out each revision in an isolated working copy, runs the command, then
|
||||
amends the revision with the resulting working copy. Descendants are rebased
|
||||
on top of the amended revisions.
|
||||
amends the revision with the resulting working copy. By default, descendants
|
||||
are rebased on top of the amended revisions, propagating the diff. Use
|
||||
`--restore-descendants` to keep descendants' content unchanged instead.
|
||||
|
||||
The command is executed with the following environment variables set:
|
||||
|
||||
@@ -3003,6 +3004,7 @@ $ jj run -j 4 -- pre-commit run .github/pre-commit.yaml
|
||||
* `--clean` — Delete each working copy before running the command
|
||||
|
||||
By default `jj run` reuses working copies between invocations so build artifacts are preserved. With `--clean`, every commit starts from a freshly checked-out tree.
|
||||
* `--restore-descendants` — Preserve the content (not the diff) when rebasing descendants
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use insta::assert_snapshot;
|
||||
|
||||
use crate::common::TestEnvironment;
|
||||
use crate::common::TestWorkDir;
|
||||
use crate::common::create_commit_with_files;
|
||||
|
||||
#[test]
|
||||
fn test_run_simple() {
|
||||
@@ -1139,6 +1140,127 @@ fn test_run_pool_failed_command_does_not_poison_slot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_restore_descendants_preserves_content() {
|
||||
let mut test_env = TestEnvironment::default();
|
||||
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
||||
let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter");
|
||||
assert!(fake_formatter.is_file());
|
||||
let fake_formatter_path = fake_formatter.to_string_lossy().into_owned();
|
||||
test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH");
|
||||
let work_dir = test_env.work_dir("repo");
|
||||
|
||||
create_commit_with_files(&work_dir, "a", &[], &[("file", "a\n")]);
|
||||
create_commit_with_files(&work_dir, "b", &["a"], &[("file", "b\n")]);
|
||||
create_commit_with_files(&work_dir, "c", &["b"], &[("file", "c\n")]);
|
||||
|
||||
let command = if cfg!(windows) {
|
||||
format!("{fake_formatter_path} --tee ran-%JJ_CHANGE_ID%.txt")
|
||||
} else {
|
||||
format!("{fake_formatter_path} --tee ran-$JJ_CHANGE_ID.txt")
|
||||
};
|
||||
let args: &[&str] = if cfg!(windows) {
|
||||
&[
|
||||
"run",
|
||||
"-r",
|
||||
"a::b",
|
||||
"--restore-descendants",
|
||||
"--",
|
||||
"cmd",
|
||||
"/c",
|
||||
command.as_str(),
|
||||
]
|
||||
} else {
|
||||
&[
|
||||
"run",
|
||||
"-r",
|
||||
"a::b",
|
||||
"--restore-descendants",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
command.as_str(),
|
||||
]
|
||||
};
|
||||
let output = work_dir.run_jj(args).success();
|
||||
assert_snapshot!(output.stderr, @r"
|
||||
Rewrote 2 commits
|
||||
Rebased 1 descendant commits (while preserving their content)
|
||||
Working copy (@) now at: royxmykx a741a7d3 c | c
|
||||
Parent commit (@-) : zsuskuln 43c5a714 b | b
|
||||
[EOF]
|
||||
");
|
||||
|
||||
assert_snapshot!(
|
||||
work_dir
|
||||
.run_jj(&["file", "list", "-r", "a"])
|
||||
.success()
|
||||
.stdout,
|
||||
@r"
|
||||
file
|
||||
ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
|
||||
[EOF]
|
||||
"
|
||||
);
|
||||
assert_snapshot!(
|
||||
work_dir
|
||||
.run_jj(&["file", "list", "-r", "b"])
|
||||
.success()
|
||||
.stdout,
|
||||
@r"
|
||||
file
|
||||
ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt
|
||||
[EOF]
|
||||
"
|
||||
);
|
||||
assert_snapshot!(
|
||||
work_dir
|
||||
.run_jj(&["file", "list", "-r", "c"])
|
||||
.success()
|
||||
.stdout,
|
||||
@r"
|
||||
file
|
||||
[EOF]
|
||||
"
|
||||
);
|
||||
|
||||
assert_snapshot!(
|
||||
work_dir.run_jj(&["diff", "--from=a", "--to=b", "--git"]).success().stdout,
|
||||
@r"
|
||||
diff --git a/file b/file
|
||||
index 7898192261..6178079822 100644
|
||||
--- a/file
|
||||
+++ b/file
|
||||
@@ -1,1 +1,1 @@
|
||||
-a
|
||||
+b
|
||||
diff --git a/ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt b/ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
|
||||
deleted file mode 100644
|
||||
index e69de29bb2..0000000000
|
||||
diff --git a/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt b/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt
|
||||
new file mode 100644
|
||||
index 0000000000..e69de29bb2
|
||||
[EOF]
|
||||
"
|
||||
);
|
||||
assert_snapshot!(
|
||||
work_dir.run_jj(&["diff", "--from=b", "--to=c", "--git"]).success().stdout,
|
||||
@r"
|
||||
diff --git a/file b/file
|
||||
index 6178079822..f2ad6c76f0 100644
|
||||
--- a/file
|
||||
+++ b/file
|
||||
@@ -1,1 +1,1 @@
|
||||
-b
|
||||
+c
|
||||
diff --git a/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt b/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt
|
||||
deleted file mode 100644
|
||||
index e69de29bb2..0000000000
|
||||
[EOF]
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_failure_shows_output() {
|
||||
let test_env = TestEnvironment::default();
|
||||
|
||||
Reference in New Issue
Block a user