diff --git a/CHANGELOG.md b/CHANGELOG.md index a7150b121..0633a6468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The new `jj print` command prints the contents of a file in a revision. +* `jj move` now lets you limit the set of changes to move by specifying paths + on the command line (in addition to the `--interactive` mode). For example, + use `jj move --to @-- foo` to move the changes to file (or directory) `foo` in + the working copy to the grandparent commit. + ### Fixed bugs * Errors are now printed to stderr (they used to be printed to stdout). diff --git a/src/commands.rs b/src/commands.rs index a858bddc5..3bd10d5e2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -653,6 +653,46 @@ impl WorkspaceCommandHelper { ) } + fn select_diff( + &self, + ui: &Ui, + left_tree: &Tree, + right_tree: &Tree, + instructions: &str, + interactive: bool, + paths: &[String], + ) -> Result { + if interactive { + Ok(crate::diff_edit::edit_diff( + &self.settings, + left_tree, + right_tree, + instructions, + self.base_ignores(), + )?) + } else if paths.is_empty() { + // Optimization for a common case + Ok(right_tree.id().clone()) + } else { + // TODO: It's probably better to have the caller pass in the matcher, but then + // we'll want to be able to check if it matches everything so we do + // the optimization above. + let matcher = matcher_from_values(ui, self.workspace_root(), paths)?; + let mut tree_builder = self.repo().store().tree_builder(left_tree.id().clone()); + for (repo_path, diff) in left_tree.diff(right_tree, matcher.as_ref()) { + match diff.into_options().1 { + Some(value) => { + tree_builder.set(repo_path, value); + } + None => { + tree_builder.remove(repo_path); + } + } + } + Ok(tree_builder.write_tree()) + } + } + fn start_transaction(&self, description: &str) -> Transaction { let mut tx = self.repo.start_transaction(description); // TODO: Either do better shell-escaping here or store the values in some list @@ -1233,6 +1273,9 @@ struct MoveArgs { /// Interactively choose which parts to move #[clap(long, short)] interactive: bool, + /// Move only changes to these paths (instead of all paths) + #[clap(conflicts_with = "interactive")] + paths: Vec, } /// Move changes from a revision into its parent @@ -3125,9 +3168,8 @@ fn cmd_move(ui: &mut Ui, command: &CommandHelper, args: &MoveArgs) -> Result<(), let repo = workspace_command.repo(); let parent_tree = merge_commit_trees(repo.as_repo_ref(), &source.parents()); let source_tree = source.tree(); - let new_parent_tree_id = if args.interactive { - let instructions = format!( - "\ + let instructions = format!( + "\ You are moving changes from: {} into commit: {} @@ -3139,13 +3181,17 @@ Adjust the right side until the diff shows the changes you want to move to the destination. If you don't make any changes, then all the changes from the source will be moved into the destination. ", - short_commit_description(&source), - short_commit_description(&destination) - ); - workspace_command.edit_diff(&parent_tree, &source_tree, &instructions)? - } else { - source_tree.id().clone() - }; + short_commit_description(&source), + short_commit_description(&destination) + ); + let new_parent_tree_id = workspace_command.select_diff( + ui, + &parent_tree, + &source_tree, + &instructions, + args.interactive, + &args.paths, + )?; if &new_parent_tree_id == parent_tree.id() { return Err(CommandError::UserError(String::from("No changes to move"))); } diff --git a/tests/test_move_command.rs b/tests/test_move_command.rs index 720010111..b1087dff6 100644 --- a/tests/test_move_command.rs +++ b/tests/test_move_command.rs @@ -145,7 +145,7 @@ fn test_move() { } #[test] -fn test_move_interactive() { +fn test_move_partial() { let mut test_env = TestEnvironment::default(); test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); let repo_path = test_env.env_root().join("repo"); @@ -156,9 +156,6 @@ fn test_move_interactive() { // D B // |/ // A - // - // When moving changes between e.g. C and F, we should not get unrelated changes - // from B and D. test_env.jj_cmd_success(&repo_path, &["branch", "a"]); std::fs::write(repo_path.join("file1"), "a\n").unwrap(); std::fs::write(repo_path.join("file2"), "a\n").unwrap(); @@ -188,7 +185,7 @@ fn test_move_interactive() { let edit_script = test_env.set_up_fake_diff_editor(); - // If we don't make any changes, the whole change is moved + // If we don't make any changes in the diff-editor, the whole change is moved std::fs::write(&edit_script, "").unwrap(); let stdout = test_env.jj_cmd_success(&repo_path, &["move", "-i", "--from", "c"]); insta::assert_snapshot!(stdout, @r###" @@ -215,7 +212,7 @@ fn test_move_interactive() { insta::assert_snapshot!(stdout, @"d "); - // Can move only part of the change + // Can move only part of the change in interactive mode test_env.jj_cmd_success(&repo_path, &["undo"]); std::fs::write(&edit_script, "reset file2").unwrap(); let stdout = test_env.jj_cmd_success(&repo_path, &["move", "-i", "--from", "c"]); @@ -240,7 +237,38 @@ fn test_move_interactive() { let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file2"]); insta::assert_snapshot!(stdout, @"a "); - // File `file2`, which was not changed in source, is unchanged + // File `file3`, which was changed in source's parent, is unchanged + let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file3"]); + insta::assert_snapshot!(stdout, @"d +"); + + // Can move only part of the change in non-interactive mode + test_env.jj_cmd_success(&repo_path, &["undo"]); + // Clear the script so we know it won't be used + std::fs::write(&edit_script, "").unwrap(); + let stdout = test_env.jj_cmd_success(&repo_path, &["move", "--from", "c", "file1"]); + insta::assert_snapshot!(stdout, @r###" + Working copy now at: 17c2e6632cc5 + Added 0 files, modified 1 files, removed 0 files + "###); + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]); + insta::assert_snapshot!(stdout, @r###" + @ 17c2e6632cc5 d + | o 6a3ae047a03e c + | o 55171e33db26 b + |/ + o 3db0a2f5b535 a + o 000000000000 + "###); + // The selected change from the source has been applied + let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file1"]); + insta::assert_snapshot!(stdout, @"c +"); + // The unselected change from the source has not been applied + let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file2"]); + insta::assert_snapshot!(stdout, @"a +"); + // File `file3`, which was changed in source's parent, is unchanged let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file3"]); insta::assert_snapshot!(stdout, @"d ");