diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb6b2ab80..42a2dc3bb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * jj now bundles a TUI tool to use as the default diff and merge editors. (The previous default was `meld`.) +* `jj split` supports the `--interactive` flag. (This is already the default if + no paths are provided.) + +* `jj commit` accepts an optional list of paths indicating a subset of files to + include in the first commit + ### Fixed bugs ## [0.9.0] - 2023-09-06 diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 9762ae5e84..f03abb33cd 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -472,6 +472,9 @@ struct CommitArgs { /// The change description to use (don't open editor) #[arg(long = "message", short, value_name = "MESSAGE")] message_paragraphs: Vec, + /// Put these paths in the first commit + #[arg(value_hint = clap::ValueHint::AnyPath)] + paths: Vec, } /// Create a new change with the same content as an existing one @@ -877,10 +880,14 @@ struct DiffeditArgs { /// asked for a description only for the first part. #[derive(clap::Args, Clone, Debug)] struct SplitArgs { + /// Interactively choose which parts to split. This is the default if no + /// paths are provided. + #[arg(long, short)] + interactive: bool, /// The revision to split #[arg(long, short, default_value = "@")] revision: RevisionArg, - /// Put these paths in the first commit and don't run the diff editor + /// Put these paths in the first commit #[arg(value_hint = clap::ValueHint::AnyPath)] paths: Vec, } @@ -2129,18 +2136,31 @@ fn cmd_commit(ui: &mut Ui, command: &CommandHelper, args: &CommitArgs) -> Result .get_wc_commit_id() .ok_or_else(|| user_error("This command requires a working copy"))?; let commit = workspace_command.repo().store().get_commit(commit_id)?; + let template = + description_template_for_commit(ui, command.settings(), &workspace_command, &commit)?; + let matcher = workspace_command.matcher_from_values(&args.paths)?; + let mut tx = workspace_command.start_transaction(&format!("commit {}", commit.id().hex())); + let base_tree = merge_commit_trees(tx.repo(), &commit.parents())?; + let tree_id = tx.select_diff(ui, &base_tree, &commit.tree()?, matcher.as_ref(), "", false)?; + let middle_tree = tx.repo().store().get_root_tree(&tree_id)?; + if !args.paths.is_empty() && middle_tree.id() == base_tree.id() { + writeln!( + ui.warning(), + "The given paths do not match any file: {}", + args.paths.join(" ") + )?; + } + let description = if !args.message_paragraphs.is_empty() { cli_util::join_message_paragraphs(&args.message_paragraphs) } else { - let template = - description_template_for_commit(ui, command.settings(), &workspace_command, &commit)?; - edit_description(workspace_command.repo(), &template, command.settings())? + edit_description(tx.base_repo(), &template, command.settings())? }; - let mut tx = workspace_command.start_transaction(&format!("commit {}", commit.id().hex())); let new_commit = tx .mut_repo() .rewrite_commit(command.settings(), &commit) + .set_tree_id(tree_id) .set_description(description) .write()?; let workspace_ids = tx @@ -2153,7 +2173,7 @@ fn cmd_commit(ui: &mut Ui, command: &CommandHelper, args: &CommitArgs) -> Result .new_commit( command.settings(), vec![new_commit.id().clone()], - new_commit.tree_id().clone(), + commit.tree_id().clone(), ) .write()?; for workspace_id in workspace_ids { @@ -3255,7 +3275,7 @@ fn cmd_split(ui: &mut Ui, command: &CommandHelper, args: &SplitArgs) -> Result<( workspace_command.start_transaction(&format!("split commit {}", commit.id().hex())); let end_tree = commit.tree()?; let base_tree = merge_commit_trees(tx.repo(), &commit.parents())?; - let interactive = args.paths.is_empty(); + let interactive = args.interactive || args.paths.is_empty(); let instructions = format!( "\ You are splitting a commit in two: {} diff --git a/cli/tests/test_commit_command.rs b/cli/tests/test_commit_command.rs index 32b3985253..a1e1acd43e 100644 --- a/cli/tests/test_commit_command.rs +++ b/cli/tests/test_commit_command.rs @@ -120,6 +120,56 @@ fn test_commit_without_working_copy() { "###); } +#[test] +fn test_commit_paths() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let workspace_path = test_env.env_root().join("repo"); + + std::fs::write(workspace_path.join("file1"), "foo\n").unwrap(); + std::fs::write(workspace_path.join("file2"), "bar\n").unwrap(); + + test_env.jj_cmd_success(&workspace_path, &["commit", "-m=first", "file1"]); + let stdout = test_env.jj_cmd_success(&workspace_path, &["diff", "-r", "@-"]); + insta::assert_snapshot!(stdout, @r###" + Added regular file file1: + 1: foo + "###); + + let stdout = test_env.jj_cmd_success(&workspace_path, &["diff"]); + insta::assert_snapshot!(stdout, @" + Added regular file file2: + 1: bar + "); +} + +#[test] +fn test_commit_paths_warning() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let workspace_path = test_env.env_root().join("repo"); + + std::fs::write(workspace_path.join("file1"), "foo\n").unwrap(); + std::fs::write(workspace_path.join("file2"), "bar\n").unwrap(); + + let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_path, &["commit", "-m=first", "file3"]); + insta::assert_snapshot!(stderr, @r###" + The given paths do not match any file: file3 + "###); + insta::assert_snapshot!(stdout, @r###" + Working copy now at: rlvkpnrz 67872820 (no description set) + Parent commit : qpvuntsm 69542c19 (empty) first + "###); + + let stdout = test_env.jj_cmd_success(&workspace_path, &["diff"]); + insta::assert_snapshot!(stdout, @r###" + Added regular file file1: + 1: foo + Added regular file file2: + 1: bar + "###); +} + fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { let template = r#"commit_id.short() ++ " " ++ description"#; test_env.jj_cmd_success(cwd, &["log", "-T", template])