Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a --siblings option to the jj split command #3037

Merged
merged 2 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* There is a new global `--quiet` flag to silence commands' non-primary output.

* `jj split` now supports a `--siblings/-s` option that splits the target
revision into siblings with the same parents and children.

### Fixed bugs

## [0.15.1] - 2024-03-06
Expand Down
48 changes: 30 additions & 18 deletions cli/src/commands/rebase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::borrow::Borrow;
use std::collections::HashMap;
use std::io::Write;
use std::sync::Arc;
Expand All @@ -30,7 +31,7 @@ use tracing::instrument;

use crate::cli_util::{
resolve_multiple_nonempty_revsets_default_single, short_commit_hash, CommandHelper,
RevisionArg, WorkspaceCommandHelper,
RevisionArg, WorkspaceCommandHelper, WorkspaceCommandTransaction,
};
use crate::command_error::{user_error, CommandError};
use crate::formatter::Formatter;
Expand Down Expand Up @@ -222,7 +223,7 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets
} else if !args.source.is_empty() {
let source_commits =
resolve_multiple_nonempty_revsets_default_single(&workspace_command, &args.source)?;
rebase_descendants(
rebase_descendants_transaction(
ui,
command.settings(),
&mut workspace_command,
Expand Down Expand Up @@ -273,7 +274,7 @@ fn rebase_branch(
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;
rebase_descendants(
rebase_descendants_transaction(
ui,
settings,
workspace_command,
Expand All @@ -283,7 +284,30 @@ fn rebase_branch(
)
}

fn rebase_descendants(
/// Rebases `old_commits` onto `new_parents`.
pub fn rebase_descendants(
tx: &mut WorkspaceCommandTransaction,
settings: &UserSettings,
new_parents: &[Commit],
old_commits: &[impl Borrow<Commit>],
rebase_options: RebaseOptions,
) -> Result<usize, CommandError> {
for old_commit in old_commits.iter() {
rebase_commit_with_options(
settings,
tx.mut_repo(),
old_commit.borrow(),
new_parents,
&rebase_options,
)?;
}
let num_rebased = old_commits.len()
+ tx.mut_repo()
.rebase_descendants_with_options(settings, rebase_options)?;
Ok(num_rebased)
}

fn rebase_descendants_transaction(
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &mut WorkspaceCommandHelper,
Expand All @@ -307,20 +331,8 @@ fn rebase_descendants(
check_rebase_destinations(workspace_command.repo(), new_parents, old_commit)?;
}
let mut tx = workspace_command.start_transaction();
// `rebase_descendants` takes care of sorting in reverse topological order, so
// no need to do it here.
for old_commit in old_commits.iter() {
rebase_commit_with_options(
settings,
tx.mut_repo(),
old_commit,
new_parents,
&rebase_options,
)?;
}
let num_rebased = old_commits.len()
+ tx.mut_repo()
.rebase_descendants_with_options(settings, rebase_options)?;
let num_rebased =
rebase_descendants(&mut tx, settings, new_parents, &old_commits, rebase_options)?;
writeln!(ui.status(), "Rebased {num_rebased} commits")?;
let tx_message = if old_commits.len() == 1 {
format!(
Expand Down
72 changes: 58 additions & 14 deletions cli/src/commands/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
// limitations under the License.
use std::io::Write;

use itertools::Itertools;
use jj_lib::commit::Commit;
use jj_lib::object_id::ObjectId;
use jj_lib::repo::Repo;
use jj_lib::revset::{RevsetExpression, RevsetIteratorExt};
use jj_lib::rewrite::merge_commit_trees;
use tracing::instrument;

use crate::cli_util::{CommandHelper, RevisionArg};
use crate::command_error::CommandError;
use crate::commands::rebase::rebase_descendants;
use crate::description_util::{description_template_for_commit, edit_description};
use crate::ui::Ui;

Expand Down Expand Up @@ -47,6 +51,9 @@ pub(crate) struct SplitArgs {
/// The revision to split
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Split the revision into two siblings instead of a parent and child.
#[arg(long, short)]
siblings: bool,
emesterhazy marked this conversation as resolved.
Show resolved Hide resolved
/// Put these paths in the first commit
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
Expand All @@ -72,13 +79,13 @@ pub(crate) fn cmd_split(
let base_tree = merge_commit_trees(tx.repo(), &commit.parents())?;
let instructions = format!(
"\
You are splitting a commit in two: {}
You are splitting a commit into two: {}

The diff initially shows the changes in the commit you're splitting.

Adjust the right side until it shows the contents you want for the first
(parent) commit. The remainder will be in the second commit. If you
don't make any changes, then the operation will be aborted.
Adjust the right side until it shows the contents you want for the first commit.
The remainder will be in the second commit. If you don't make any changes, then
the operation will be aborted.
",
tx.format_commit_summary(&commit)
);
Expand Down Expand Up @@ -106,7 +113,7 @@ don't make any changes, then the operation will be aborted.
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the first part (parent).",
"Enter a description for the first commit.",
commit.description(),
&base_tree,
&selected_tree,
Expand All @@ -119,7 +126,21 @@ don't make any changes, then the operation will be aborted.
.set_description(first_description)
.write()?;

// Create the second commit, which includes everything the user didn't select.
// Create the second commit, which includes everything the user didn't
// select.
let (second_tree, second_base_tree) = if args.siblings {
// Merge the original commit tree with its parent using the tree
// containing the user selected changes as the base for the merge.
// This results in a tree with the changes the user didn't select.
(end_tree.merge(&selected_tree, &base_tree)?, &base_tree)
} else {
(end_tree, &selected_tree)
};
let second_commit_parents = if args.siblings {
commit.parent_ids().to_vec()
} else {
vec![first_commit.id().clone()]
};
let second_description = if commit.description().is_empty() {
// If there was no description before, don't ask for one for the second commit.
"".to_string()
Expand All @@ -128,27 +149,50 @@ don't make any changes, then the operation will be aborted.
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the second part (child).",
"Enter a description for the second commit.",
commit.description(),
&selected_tree,
&end_tree,
second_base_tree,
&second_tree,
)?;
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_id(commit.tree_id().clone())
.set_parents(second_commit_parents)
.set_tree_id(second_tree.id())
// Generate a new change id so that the commit being split doesn't
// become divergent.
.generate_new_change_id()
.set_description(second_description)
.write()?;

// We want only the `second_commit` to inherit `commit`'s branches and
// descendants.
// Mark the commit being split as rewritten to the second commit. As a
// result, if @ points to the commit being split, it will point to the
// second commit after the command finishes. This also means that any
// branches pointing to the commit being split are moved to the second
// commit.
tx.mut_repo()
.set_rewritten_commit(commit.id().clone(), second_commit.id().clone());
let num_rebased = tx.mut_repo().rebase_descendants(command.settings())?;
// Rebase descendants of the commit being split.
let new_parents = if args.siblings {
vec![first_commit.clone(), second_commit.clone()]
} else {
vec![second_commit.clone()]
};
let children: Vec<Commit> = RevsetExpression::commit(commit.id().clone())
.children()
.evaluate_programmatic(tx.base_repo().as_ref())?
.iter()
.commits(tx.base_repo().store())
.try_collect()?;
let num_rebased = rebase_descendants(
&mut tx,
command.settings(),
&new_parents,
&children,
Default::default(),
)?;
if let Some(mut formatter) = ui.status_formatter() {
if num_rebased > 0 {
writeln!(formatter, "Rebased {num_rebased} descendant commits")?;
Expand Down
4 changes: 4 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,10 @@ If the change you split had a description, you will be asked to enter a change d
* `-r`, `--revision <REVISION>` — The revision to split

Default value: `@`
* `-s`, `--siblings` — Split the revision into two siblings instead of a parent and child

Possible values: `true`, `false`




Expand Down
Loading