Repository: jj-vcs/jj (Rust)

Issue:
`jj split .` should create an empty commit, and `jj split nonexistent_file` should be easier
1. Currently, `jj split . -r @-` results in a message saying `Nothing changed.`. It should instead create an empty commit after the commit being split. This is very similar to https://github.com/martinvonz/jj/issues/787.

    Attempting to do this with interactive  `jj split` by changing nothing and exiting also currently results in the `Nothing changed` message. This behavior should probably be kept, but perhaps in this case there could be a UI hint to use `jj split .` in case that's what the user actually wanted to do.

    Note that this is different from `jj new` behavior if the commit has any children. Currently, the best workaround I know is to use `jj new` followed by `jj rebase` in this case.

4.  `jj split nonexistent_file -r @-` does create an empty commit before the commit being split, as @martinvonz once taught me, which is useful (especially for merge commits). I think there should be an easier way, e.g. `jj split --none -r @-`.

    For the record, currently, another way to achieve this is to use interactive `jj split` and undo all the changes there.



Discussion:
Sounds good. Allowing `jj split .` seems consistent with allowing `jj squash` on an empty commit, as you noted.

I wonder if there's a more intuitive way of achieving what the user intended. `jj new --insert`? What do you typically do after inserting the empty commit? Do you edit it and let the every command rebase the descendants or do you check it out and squash in changes once you're done?
For inserting above a commit (`jj split .` case), I usually want it when I have a structure like

    a b
     \|
    commit to split

I think `jj new --insert` would work well here.

Conversely,  `jj split nonexistent`  is useful for merge commits. I'm not sure if adding this as `jj new --insert-below` makes sense. Perhaps we can think of something better, or maybe `jj split --none` is good enough.




> What do you typically do after inserting the empty commit? Do you edit it and let the every command rebase the descendants or do you check it out and squash in changes once you're done?

To actually answer your question: I check it out and squash when I want a rebase to happen. When I `jj edit` directly, I'm worried about races between VS Code and jj being more common, for instance because the rebases get slower.
> > What do you typically do after inserting the empty commit? Do you edit it and let the every command rebase the descendants or do you check it out and squash in changes once you're done?
> 
> To actually answer your question: I check it out and squash when I want a rebase to happen. When I `jj edit` directly, I'm worried about races between VS Code and jj being more common, for instance because the rebases get slower.

In that case, `jj new --insert` wouldn't really be what you want because that would presumably edit the new commit. The `new` followed by `rebase` you mentioned actually sounds pretty good then. Is your issue with it that it doesn't match your mental model as well?
Yes, I wouldn't want to immediately edit that commit, though it wouldn't be much of a burden to do another `jj new`.

The reason I want this is that `commit to split` might have many children, and I'd have to rebase each one.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d6005eeb4d..f6bfa988ff9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -89,6 +89,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * Commands that draw an ASCII graph (`jj log`, `jj op log`, `jj obslog`) now
   have different styles available by setting e.g. `ui.graph.style = "curved"`.
 
+* `jj split` accepts creating empty commits when given a path. `jj split .`
+  inserts an empty commit between the target commit and its children if any,
+  and `jj split any-non-existent-path` inserts an empty commit between the
+  target commit and its parents.
+
 ### Fixed bugs
 
 * When sharing the working copy with a Git repo, we used to forget to export
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 13c5fb2f16f..88ac66d828d 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -2557,6 +2557,7 @@ fn cmd_split(ui: &mut Ui, command: &CommandHelper, args: &SplitArgs) -> Result<(
     let mut tx =
         workspace_command.start_transaction(&format!("split commit {}", commit.id().hex()));
     let base_tree = merge_commit_trees(tx.base_repo().as_repo_ref(), &commit.parents());
+    let interactive = args.paths.is_empty();
     let instructions = format!(
         "\
 You are splitting a commit in two: {}
@@ -2574,67 +2575,66 @@ don't make any changes, then the operation will be aborted.
         &base_tree,
         &commit.tree(),
         &instructions,
-        args.paths.is_empty(),
+        interactive,
         matcher.as_ref(),
     )?;
-    if &tree_id == commit.tree_id() {
+    if &tree_id == commit.tree_id() && interactive {
         ui.write("Nothing changed.\n")?;
-    } else {
-        let middle_tree = tx
-            .base_repo()
-            .store()
-            .get_tree(&RepoPath::root(), &tree_id)?;
-
-        let first_template = description_template_for_cmd_split(
-            tx.base_workspace_helper(),
-            "Enter commit description for the first part (parent).",
-            commit.description(),
-            &base_tree,
-            &middle_tree,
-        )?;
-        let first_description =
-            edit_description(tx.base_repo(), &first_template, command.settings())?;
-        let first_commit = tx
-            .mut_repo()
-            .rewrite_commit(command.settings(), &commit)
-            .set_tree(tree_id)
-            .set_description(first_description)
-            .write()?;
-        let second_template = description_template_for_cmd_split(
-            tx.base_workspace_helper(),
-            "Enter commit description for the second part (child).",
-            commit.description(),
-            &middle_tree,
-            &commit.tree(),
-        )?;
-        let second_description =
-            edit_description(tx.base_repo(), &second_template, command.settings())?;
-        let second_commit = tx
-            .mut_repo()
-            .rewrite_commit(command.settings(), &commit)
-            .set_parents(vec![first_commit.id().clone()])
-            .set_tree(commit.tree_id().clone())
-            .generate_new_change_id()
-            .set_description(second_description)
-            .write()?;
-        let mut rebaser = DescendantRebaser::new(
-            command.settings(),
-            tx.mut_repo(),
-            hashmap! { commit.id().clone() => hashset!{second_commit.id().clone()} },
-            hashset! {},
-        );
-        rebaser.rebase_all()?;
-        let num_rebased = rebaser.rebased().len();
-        if num_rebased > 0 {
-            writeln!(ui, "Rebased {num_rebased} descendant commits")?;
-        }
-        ui.write("First part: ")?;
-        tx.write_commit_summary(ui.stdout_formatter().as_mut(), &first_commit)?;
-        ui.write("\nSecond part: ")?;
-        tx.write_commit_summary(ui.stdout_formatter().as_mut(), &second_commit)?;
-        ui.write("\n")?;
-        tx.finish(ui)?;
+        return Ok(());
     }
+    let middle_tree = tx
+        .base_repo()
+        .store()
+        .get_tree(&RepoPath::root(), &tree_id)?;
+
+    let first_template = description_template_for_cmd_split(
+        tx.base_workspace_helper(),
+        "Enter commit description for the first part (parent).",
+        commit.description(),
+        &base_tree,
+        &middle_tree,
+    )?;
+    let first_description = edit_description(tx.base_repo(), &first_template, command.settings())?;
+    let first_commit = tx
+        .mut_repo()
+        .rewrite_commit(command.settings(), &commit)
+        .set_tree(tree_id)
+        .set_description(first_description)
+        .write()?;
+    let second_template = description_template_for_cmd_split(
+        tx.base_workspace_helper(),
+        "Enter commit description for the second part (child).",
+        commit.description(),
+        &middle_tree,
+        &commit.tree(),
+    )?;
+    let second_description =
+        edit_description(tx.base_repo(), &second_template, command.settings())?;
+    let second_commit = tx
+        .mut_repo()
+        .rewrite_commit(command.settings(), &commit)
+        .set_parents(vec![first_commit.id().clone()])
+        .set_tree(commit.tree_id().clone())
+        .generate_new_change_id()
+        .set_description(second_description)
+        .write()?;
+    let mut rebaser = DescendantRebaser::new(
+        command.settings(),
+        tx.mut_repo(),
+        hashmap! { commit.id().clone() => hashset!{second_commit.id().clone()} },
+        hashset! {},
+    );
+    rebaser.rebase_all()?;
+    let num_rebased = rebaser.rebased().len();
+    if num_rebased > 0 {
+        writeln!(ui, "Rebased {num_rebased} descendant commits")?;
+    }
+    ui.write("First part: ")?;
+    tx.write_commit_summary(ui.stdout_formatter().as_mut(), &first_commit)?;
+    ui.write("\nSecond part: ")?;
+    tx.write_commit_summary(ui.stdout_formatter().as_mut(), &second_commit)?;
+    ui.write("\n")?;
+    tx.finish(ui)?;
     Ok(())
 }
 


Test patch:
diff --git a/tests/test_split_command.rs b/tests/test_split_command.rs
index b0be6776512..82190bdfc3f 100644
--- a/tests/test_split_command.rs
+++ b/tests/test_split_command.rs
@@ -26,9 +26,9 @@ fn test_split_by_paths() {
     std::fs::write(repo_path.join("file2"), "foo").unwrap();
     std::fs::write(repo_path.join("file3"), "foo").unwrap();
 
-    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "commit_id.short()"]);
+    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "change_id.short()"]);
     insta::assert_snapshot!(stdout, @r###"
-    @ 9d08ea8cac40
+    @ 9a45c67d3e96
     o 000000000000
     "###);
 
@@ -64,10 +64,10 @@ fn test_split_by_paths() {
     JJ: Lines starting with "JJ: " (like this one) will be removed.
     "###);
 
-    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "commit_id.short()"]);
+    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "change_id.short()"]);
     insta::assert_snapshot!(stdout, @r###"
-    @ 45833353d94e
-    o 5eebce1de3b0
+    @ ffdaa62087a2
+    o 9a45c67d3e96
     o 000000000000
     "###);
 
@@ -80,4 +80,55 @@ fn test_split_by_paths() {
     A file1
     A file3
     "###);
+
+    // Insert an empty commit after @- with "split ."
+    test_env.set_up_fake_editor();
+    let stdout = test_env.jj_cmd_success(&repo_path, &["split", "-r", "@-", "."]);
+    insta::assert_snapshot!(stdout, @r###"
+    Rebased 1 descendant commits
+    First part: 31425b568fcf (no description set)
+    Second part: af0963926ac3 (no description set)
+    Working copy now at: 28d4ec20efa9 (no description set)
+    "###);
+
+    let stdout =
+        test_env.jj_cmd_success(&repo_path, &["log", "-T", r#"change_id.short() " " empty"#]);
+    insta::assert_snapshot!(stdout, @r###"
+    @ ffdaa62087a2 false
+    o 19b790168e73 true
+    o 9a45c67d3e96 false
+    o 000000000000 true
+    "###);
+
+    let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s", "-r", "@--"]);
+    insta::assert_snapshot!(stdout, @r###"
+    A file2
+    "###);
+
+    // Remove newly created empty commit
+    test_env.jj_cmd_success(&repo_path, &["abandon", "@-"]);
+
+    // Insert an empty commit before @- with "split nonexistent"
+    test_env.set_up_fake_editor();
+    let stdout = test_env.jj_cmd_success(&repo_path, &["split", "-r", "@-", "nonexistent"]);
+    insta::assert_snapshot!(stdout, @r###"
+    Rebased 1 descendant commits
+    First part: 0647b2cbd0da (no description set)
+    Second part: d5d77af65446 (no description set)
+    Working copy now at: 86f228dc3a50 (no description set)
+    "###);
+
+    let stdout =
+        test_env.jj_cmd_success(&repo_path, &["log", "-T", r#"change_id.short() " " empty"#]);
+    insta::assert_snapshot!(stdout, @r###"
+    @ ffdaa62087a2 false
+    o fa9213bcf78e false
+    o 9a45c67d3e96 true
+    o 000000000000 true
+    "###);
+
+    let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s", "-r", "@-"]);
+    insta::assert_snapshot!(stdout, @r###"
+    A file2
+    "###);
 }