Skip to content

Commit

Permalink
Implement jj parallelize
Browse files Browse the repository at this point in the history
  • Loading branch information
emesterhazy committed Mar 31, 2024
1 parent 71b65e5 commit 5998767
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 0 deletions.
5 changes: 5 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod new;
mod next;
mod obslog;
mod operation;
mod parallelize;
mod prev;
mod rebase;
mod resolve;
Expand Down Expand Up @@ -114,6 +115,7 @@ enum Command {
#[command(subcommand)]
#[command(visible_alias = "op")]
Operation(operation::OperationCommand),
Parallelize(parallelize::ParallelizeArgs),
Prev(prev::PrevArgs),
Rebase(rebase::RebaseArgs),
Resolve(resolve::ResolveArgs),
Expand Down Expand Up @@ -180,6 +182,9 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
Command::Abandon(sub_args) => abandon::cmd_abandon(ui, command_helper, sub_args),
Command::Edit(sub_args) => edit::cmd_edit(ui, command_helper, sub_args),
Command::Next(sub_args) => next::cmd_next(ui, command_helper, sub_args),
Command::Parallelize(sub_args) => {
parallelize::cmd_parallelize(ui, command_helper, sub_args)
}
Command::Prev(sub_args) => prev::cmd_prev(ui, command_helper, sub_args),
Command::New(sub_args) => new::cmd_new(ui, command_helper, sub_args),
Command::Move(sub_args) => r#move::cmd_move(ui, command_helper, sub_args),
Expand Down
163 changes: 163 additions & 0 deletions cli/src/commands/parallelize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright 2024 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io::Write;

use indexmap::IndexSet;
use itertools::Itertools;
use jj_lib::backend::CommitId;
use jj_lib::commit::Commit;
use jj_lib::repo::Repo;
use jj_lib::revset::{RevsetExpression, RevsetIteratorExt};
use tracing::instrument;

use crate::cli_util::{short_commit_hash, CommandHelper, RevisionArg};
use crate::command_error::{user_error, CommandError};
use crate::commands::rebase::rebase_descendants;
use crate::ui::Ui;

/// Parallelize revisions by making them siblings.
///
/// The set of target commits being parallelized must have a single head and
/// root commit (not to be confused with the repo root), otherwise the command
/// will fail. Each of the target commits is rebased onto the parents of the
/// root commit. The children of the head commit are rebased onto the target
/// commits.
///
/// Example usage:
/// ```text
/// jj log
/// ◉ 3
/// ◉ 2
/// ◉ 1
/// ◉
/// jj parallelize 1::2
/// jj log
/// ◉ 3
/// ├─╮
/// │ ◉ 2
/// ◉ │ 1
/// ├─╯
/// ◉
/// ```
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
pub(crate) struct ParallelizeArgs {
// #[arg(long, short)]
revisions: Vec<RevisionArg>,
}

#[instrument(skip_all)]
pub(crate) fn cmd_parallelize(
ui: &mut Ui,
command: &CommandHelper,
args: &ParallelizeArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
// Parse and validate the input revset(s).
// Find the parents of each commit. The parent(s) which are ancestors of
// each revision are the new parents of the input revisions.
// Find the children of each commit. The children which are descendants
// of each revision are the new children of the input revisions.
// If one or both of these sets cannot be found, then parallelize cannot
// proceed.
// let target_commits = workspace_command.parse_union_revsets(&args.revisions)?.
let mut tx = workspace_command.start_transaction();
let target_revset = tx
.base_workspace_helper()
.parse_union_revsets(&args.revisions)?;
let target_commits: Vec<Commit> = tx
.base_workspace_helper()
.evaluate_revset(target_revset.clone())?
.iter()
// We want parents before children, so we need to reverse the order.
.reversed()
.commits(tx.base_repo().store())
.try_collect::<_, Vec<Commit>, _>()?;
if target_commits.len() < 2 {
return Ok(writeln!(ui.stderr(), "Nothing to do.")?);
}
let target_heads: Vec<CommitId> = tx
.base_workspace_helper()
.evaluate_revset(target_revset.clone().heads())?
.iter()
.collect();
if target_heads.len() > 1 {
let heads = target_heads.iter().map(short_commit_hash).join(", ");
return Err(user_error(format!(
"Cannot parallelize a set of revisions with multiple heads. Heads: {heads}"
)));
}
let target_roots: Vec<CommitId> = tx
.base_workspace_helper()
.evaluate_revset(target_revset.roots())?
.iter()
.collect();
if target_roots.len() > 1 {
let roots = target_roots.iter().map(short_commit_hash).join(", ");
return Err(user_error(format!(
"Cannot parallelize a set of revisions with multiple roots. Roots: {roots}"
)));
}

// Rebase the children of the head commit onto the target commits.
let new_children: Vec<Commit> = RevsetExpression::commit(target_heads[0].clone())
.children()
.evaluate_programmatic(tx.base_repo().as_ref())?
.iter()
.commits(tx.base_repo().store())
.try_collect()?;
for child in new_children {
// The ordering here is intentional. We iterate over the target commits first so
// that when `jj log` prints the graph they will be printed in their original
// order. After that, we add any existing parents which aren't being
// parallelized.
let get_id = |c: &Commit| c.id().clone();
let new_parents_for_child: Vec<Commit> = target_commits
.iter()
.map(get_id)
.chain(child.parents().iter().map(get_id))
.collect::<IndexSet<CommitId>>()
.iter()
.map(|c| tx.mut_repo().store().get_commit(c))
.collect::<Result<Vec<Commit>, _>>()?;
rebase_descendants(
&mut tx,
command.settings(),
&new_parents_for_child,
&[child],
Default::default(),
)?;
}

// Rebase the target commits onto the parents of the root commit.
let new_parents: Vec<Commit> = RevsetExpression::commit(target_roots[0].clone())
.parents()
.evaluate_programmatic(tx.base_repo().as_ref())?
.iter()
.commits(tx.base_repo().store())
.try_collect()?;
rebase_descendants(
&mut tx,
command.settings(),
&new_parents,
&target_commits,
Default::default(),
)?;

tx.finish(
ui,
format!("Parallelized {} commits.", target_commits.len()),
)
}
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod test_new_command;
mod test_next_prev_commands;
mod test_obslog_command;
mod test_operations;
mod test_parallelize_command;
mod test_rebase_command;
mod test_repo_change_report;
mod test_resolve_command;
Expand Down
181 changes: 181 additions & 0 deletions cli/tests/test_parallelize_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright 2024 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::Path;

use crate::common::TestEnvironment;

#[test]
fn test_parallelize_no_descendants() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let workspace_path = test_env.env_root().join("repo");

for n in 1..6 {
test_env.jj_cmd_ok(&workspace_path, &["commit", &format!("-m{n}")]);
}
test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=6"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ b911505e443e 6
◉ 2e00cb15c7b6 5
◉ 9df3c87db1a2 4
◉ 9f5b59fa4622 3
◉ d826910d21fb 2
◉ dc0e5d6135ce 1
◉ 000000000000
"###);

test_env.jj_cmd_ok(&workspace_path, &["parallelize", "dc0::"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ 6c7b60a45eb6 6
│ ◉ 296f48966777 5
├─╯
│ ◉ 524062469789 4
├─╯
│ ◉ a9334ecaa379 3
├─╯
│ ◉ 3a7b37ebe843 2
├─╯
│ ◉ 761e67df44b7 1
├─╯
◉ 000000000000
"###);
}

// Only the head commit has descendants.
#[test]
fn test_parallelize_with_descendants_simple() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let workspace_path = test_env.env_root().join("repo");

for n in 1..6 {
test_env.jj_cmd_ok(&workspace_path, &["commit", &format!("-m{n}")]);
}
test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=6"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ b911505e443e 6
◉ 2e00cb15c7b6 5
◉ 9df3c87db1a2 4
◉ 9f5b59fa4622 3
◉ d826910d21fb 2
◉ dc0e5d6135ce 1
◉ 000000000000
"###);

test_env.jj_cmd_ok(&workspace_path, &["parallelize", "dc0::9df"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ f28f986c7134 6
◉ 21e9963ac5ff 5
├─┬─┬─╮
│ │ │ ◉ 524062469789 4
│ │ ◉ │ a9334ecaa379 3
│ │ ├─╯
│ ◉ │ 3a7b37ebe843 2
│ ├─╯
◉ │ 761e67df44b7 1
├─╯
◉ 000000000000
"###);
}

// More than one commit being parallelized has descendants, but only the
// descendants of the head commit will be rebased onto the target commits.
#[test]
fn test_parallelize_with_descendants_complex() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let workspace_path = test_env.env_root().join("repo");

for n in 1..6 {
test_env.jj_cmd_ok(&workspace_path, &["commit", &format!("-m{n}")]);
}
test_env.jj_cmd_ok(&workspace_path, &["new", "d82", "-m=child-of-2"]);
test_env.jj_cmd_ok(&workspace_path, &["new", "2e0", "-m=6"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ d27ee705f7a9 6
◉ 2e00cb15c7b6 5
◉ 9df3c87db1a2 4
◉ 9f5b59fa4622 3
│ ◉ 60c93e41d09c child-of-2
├─╯
◉ d826910d21fb 2
◉ dc0e5d6135ce 1
◉ 000000000000
"###);

test_env.jj_cmd_ok(&workspace_path, &["parallelize", "dc0::9df"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ 9f1bec0d6c46 6
◉ 7dd2f5648395 5
├─┬─┬─╮
│ │ │ ◉ b8f977c12383 4
│ │ ◉ │ 7be8374575b9 3
│ │ ├─╯
◉ │ │ 2bfe3fe3e472 1
├───╯
│ │ ◉ bf7eef1f1dc6 child-of-2
│ ├─╯
│ ◉ 96ce11389312 2
├─╯
◉ 000000000000
"###);
}

#[test]
fn test_parallelize_failure_multiple_heads() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let workspace_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=1"]);
test_env.jj_cmd_ok(&workspace_path, &["new", "root()", "-m=2"]);
insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ 9922fc972ef6 2
│ ◉ dc0e5d6135ce 1
├─╯
◉ 000000000000
"###);

insta::assert_snapshot!(test_env.jj_cmd_failure(&workspace_path, &["parallelize", "dc0", "992"]),@r###"
Error: Cannot parallelize a set of revisions with multiple heads. Heads: 9922fc972ef6, dc0e5d6135ce
"###);
}

#[test]
fn test_parallelize_failure_multiple_roots() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let workspace_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&workspace_path, &["describe", "-m=1"]);
test_env.jj_cmd_ok(&workspace_path, &["new", "root()", "-m=2"]);
test_env.jj_cmd_ok(&workspace_path, &["new", "all:root()..", "-m=3"]);

insta::assert_snapshot!(get_log_output(&test_env, &workspace_path), @r###"
@ 42080ac7cc83 3
├─╮
│ ◉ dc0e5d6135ce 1
◉ │ 9922fc972ef6 2
├─╯
◉ 000000000000
"###);

insta::assert_snapshot!(test_env.jj_cmd_failure(&workspace_path, &["parallelize", "..@"]),@r###"
Error: Cannot parallelize a set of revisions with multiple roots. Roots: 9922fc972ef6, dc0e5d6135ce
"###);
}

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])
}

0 comments on commit 5998767

Please sign in to comment.