diff --git a/cli/src/commands/run.rs b/cli/src/commands/run.rs index 95a4b2be37c..029bed9311c 100644 --- a/cli/src/commands/run.rs +++ b/cli/src/commands/run.rs @@ -69,6 +69,7 @@ use crate::cli_util::WorkspaceCommandHelper; use crate::cli_util::WorkspaceCommandTransaction; use crate::command_error::CommandError; use crate::command_error::CommandErrorKind; +use crate::commands::sparse::SparseSetArgs; use crate::ui::Ui; #[derive(Debug, thiserror::Error)] @@ -168,6 +169,15 @@ struct WorkspacePool { /// When true, wipe each slot's working copy on acquisition so every commit /// starts from a freshly checked-out tree (no artifact reuse). clean: bool, + // TODO: (TBD) should we just define our own args? Currently, help seems + // to lack sufficient information on these options, yet using the whole + // sparse::set subcommand smells like poor UI as well. + // + // Another thing to discuss is the apparent poor performance of --remove + // option as compared to a semantically equivalent set of --clear and --add + // options, so we just might want to steer the user towards only using the + // latter two. + sparse_args: Option, } impl WorkspacePool { @@ -176,6 +186,7 @@ impl WorkspacePool { size: NonZeroUsize, auto_tracking_matcher: Box, clean: bool, + sparse_args: Option, ) -> Result { // The parent() call is needed to not write under `.jj/repo/`. let base_path = repo_path.parent().unwrap().join("run").join("default"); @@ -185,6 +196,7 @@ impl WorkspacePool { size, auto_tracking_matcher, clean, + sparse_args, }) } @@ -259,6 +271,32 @@ impl WorkspacePool { ) }; + // TODO: Sparse patterns will persist across `run` invocations, unless --clean + // is passed, which almost certainly violates the principle of least + // surprise. Persistence should probably be opt-in (and perhaps even + // required opt-in per `run` invocation?). TBD. + if let Some(sparse_args) = &self.sparse_args { + // Adopted almost verbatim from `commands::sparse::cmd_sparse_set`. + let new_patterns: Vec<_> = { + let mut new = HashSet::new(); + if !sparse_args.clear { + new.extend(tree_state.sparse_patterns().iter().cloned()); + for path in &sparse_args.remove { + new.remove(path); + } + } + for path in &sparse_args.add { + new.insert(path.to_owned()); + } + new.into_iter().sorted_unstable().collect() + }; + + // TODO: define a separate error. + tree_state + .set_sparse_patterns(new_patterns) + .map_err(|_| RunError::FailedCheckout(commit.id().clone()))?; + } + tree_state .check_out(&commit.tree()) .map_err(|_| RunError::FailedCheckout(commit.id().clone()))?; @@ -660,6 +698,9 @@ pub struct RunArgs { /// `jj run` itself. #[arg(long)] ignore_errors: bool, + + #[clap(flatten)] + sparse_args: Option, } /// Precedence: `--jobs`, `run.jobs` config, 1. @@ -769,6 +810,7 @@ pub async fn cmd_run( jobs, auto_tracking_matcher, args.clean, + args.sparse_args.clone(), )?); let spec = Arc::new(CommandSpec { diff --git a/cli/src/commands/sparse/mod.rs b/cli/src/commands/sparse/mod.rs index d9ac0817aad..e3d83cf2f7b 100644 --- a/cli/src/commands/sparse/mod.rs +++ b/cli/src/commands/sparse/mod.rs @@ -27,7 +27,7 @@ use self::list::SparseListArgs; use self::list::cmd_sparse_list; use self::reset::SparseResetArgs; use self::reset::cmd_sparse_reset; -use self::set::SparseSetArgs; +pub(crate) use self::set::SparseSetArgs; use self::set::cmd_sparse_set; use crate::cli_util::CommandHelper; use crate::cli_util::WorkspaceCommandHelper; diff --git a/cli/src/commands/sparse/set.rs b/cli/src/commands/sparse/set.rs index 9e581845df0..f5149b8b63d 100644 --- a/cli/src/commands/sparse/set.rs +++ b/cli/src/commands/sparse/set.rs @@ -36,7 +36,7 @@ pub struct SparseSetArgs { value_hint = clap::ValueHint::AnyPath, value_parser = |s: &str| RepoPathBuf::from_relative_path(s), )] - add: Vec, + pub(crate) add: Vec, /// Patterns to remove from the working copy #[arg( @@ -45,11 +45,11 @@ pub struct SparseSetArgs { value_hint = clap::ValueHint::AnyPath, value_parser = |s: &str| RepoPathBuf::from_relative_path(s), )] - remove: Vec, + pub(crate) remove: Vec, /// Include no files in the working copy (combine with --add) #[arg(long)] - clear: bool, + pub(crate) clear: bool, } #[instrument(skip_all)] diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index d03d1c88b3b..4662e6dc620 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -3040,6 +3040,9 @@ $ jj run -j 4 -- pre-commit run .github/pre-commit.yaml Any changes made by a failed command will not be saved, but changes from successful commands are still applied atomically at the end. The exit code from any failed command will not impact the exit code of `jj run` itself. +* `--add ` — Patterns to add to the working copy +* `--remove ` — Patterns to remove from the working copy +* `--clear` — Include no files in the working copy (combine with --add) diff --git a/cli/tests/test_run_command.rs b/cli/tests/test_run_command.rs index b65d4816662..ea09b5e595b 100644 --- a/cli/tests/test_run_command.rs +++ b/cli/tests/test_run_command.rs @@ -1833,3 +1833,98 @@ fn test_run_on_conflicted_commit() { [EOF] "); } + +#[test] +fn test_run_sparse() { + let test_env = TestEnvironment::default(); + test_env.run_jj_in(".", ["git", "init", "repo"]).success(); + let work_dir = test_env.work_dir("repo"); + work_dir.write_file("a", ""); + work_dir.run_jj(&["commit", "-m", "A"]).success(); + work_dir.write_file("b", ""); + work_dir.run_jj(&["commit", "-m", "B"]).success(); + work_dir.write_file("c", ""); + work_dir.run_jj(&["commit", "-m", "C"]).success(); + insta::assert_snapshot!(get_log_output(&work_dir), @r" + @ zsuskulnrvyrovkzqrwmxqlsskqntxvp + ○ kkmpptxzrspxrzommnulwmwkkqwworplC + │ + ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB + │ + ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA + │ + ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz + [EOF] + "); + let stdout = work_dir + .run_jj(&["run", "-r", "..@", "--", "ls"]) + .success() + .stdout; + insta::assert_snapshot!(stdout, @r" + a + a + b + a + b + c + a + b + c + [EOF] + "); + + let stdout = work_dir + .run_jj(&[ + "run", "-r", "..@", "--clear", "--add", "a", "--add", "b", "--", "ls", + ]) + .success() + .stdout; + insta::assert_snapshot!(stdout, @r" + a + a + b + a + b + a + b + [EOF] + "); + + let stdout = work_dir + .run_jj(&["run", "-r", "..@", "--", "ls"]) + .success() + .stdout; + insta::assert_snapshot!(stdout, @r" + a + a + b + a + b + a + b + [EOF] + "); + + let stdout = work_dir + .run_jj(&["run", "-r", "..@", "--clear", "--", "ls"]) + .success() + .stdout; + insta::assert_snapshot!(stdout, @""); + + let stdout = work_dir + .run_jj(&["run", "-r", "..@", "--clean", "--", "ls"]) + .success() + .stdout; + insta::assert_snapshot!(stdout, @r" + a + a + b + a + b + c + a + b + c + [EOF] + "); +}