156 lines · 6.2 KB · Rust
RawDownload
// 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::collections::HashMap; use clap_complete::ArgValueCompleter;use futures::TryStreamExt as _;use indexmap::IndexSet;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::commit::CommitIteratorExt as _;use tracing::instrument; use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::command_error::CommandError;use crate::complete;use crate::ui::Ui; /// Parallelize revisions by making them siblings////// Running `jj parallelize 1::2` will transform the history like this:/// ```text/// 3/// |             3/// 2            / \/// |    ->     1   2/// 1            \ //// |             0/// 0/// ```////// The command effectively says "these revisions are actually independent",/// meaning that they should no longer be ancestors/descendants of each other./// However, revisions outside the set that were previously ancestors of a/// revision in the set will remain ancestors of it. For example, revision 0/// above remains an ancestor of both 1 and 2. Similarly,/// revisions outside the set that were previously descendants of a revision/// in the set will remain descendants of it. For example, revision 3 above/// remains a descendant of both 1 and 2.////// Therefore, `jj parallelize '1 | 3'` is a no-op. That's because 2, which is/// not in the target set, was a descendant of 1 before, so it remains a/// descendant, and it was an ancestor of 3 before, so it remains an ancestor.#[derive(clap::Args, Clone, Debug)]#[command(verbatim_doc_comment)]pub(crate) struct ParallelizeArgs {    /// The revisions to parallelize [aliases: -r]    #[arg(value_name = "REVSETS")]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]    revisions_pos: Vec<RevisionArg>,     #[arg(short = 'r', hide = true, value_name = "REVSETS")]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]    revisions_opt: Vec<RevisionArg>,} #[instrument(skip_all)]pub(crate) async fn cmd_parallelize(    ui: &mut Ui,    command: &CommandHelper,    args: &ParallelizeArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui).await?;    // The target commits are the commits being parallelized. They are ordered    // here with children before parents.    let target_commits: Vec<Commit> = workspace_command        .parse_union_revsets(ui, &[&*args.revisions_pos, &*args.revisions_opt].concat())?        .evaluate_to_commits()?        .try_collect()        .await?;     // New parents for commits in the target set. Since commits in the set are now    // supposed to be independent, they inherit the parent's non-target parents,    // recursively.    let mut new_target_parents: HashMap<CommitId, Vec<CommitId>> = HashMap::new();    let mut needs_rewrite = Vec::new();    for commit in target_commits.iter().rev() {        let mut new_parents = vec![];        for old_parent in commit.parent_ids() {            if let Some(grand_parents) = new_target_parents.get(old_parent) {                new_parents.extend_from_slice(grand_parents);                needs_rewrite.push(commit.id());            } else {                new_parents.push(old_parent.clone());            }        }        new_target_parents.insert(commit.id().clone(), new_parents);    }     workspace_command.check_rewritable(needs_rewrite).await?;    let mut tx = workspace_command.start_transaction();     // If a commit outside the target set has a commit in the target set as parent,    // then - after the transformation - it should also have that commit's    // parents as direct parents, if those commits are also in the target set.    let mut new_child_parents: HashMap<CommitId, IndexSet<CommitId>> = HashMap::new();    for commit in target_commits.iter().rev() {        let mut new_parents = IndexSet::new();        for old_parent in commit.parent_ids() {            if let Some(parents) = new_child_parents.get(old_parent) {                new_parents.extend(parents.iter().cloned());            }        }        new_parents.insert(commit.id().clone());        new_child_parents.insert(commit.id().clone(), new_parents);    }     tx.repo_mut()        .transform_descendants(            target_commits.iter().ids().cloned().collect_vec(),            async |mut rewriter| {                // Commits in the target set do not depend on each other but they still depend                // on other parents                if let Some(new_parents) = new_target_parents.get(rewriter.old_commit().id()) {                    rewriter.set_new_rewritten_parents(new_parents);                } else if rewriter                    .old_commit()                    .parent_ids()                    .iter()                    .any(|id| new_child_parents.contains_key(id))                {                    let mut new_parents = vec![];                    for parent in rewriter.old_commit().parent_ids() {                        if let Some(parents) = new_child_parents.get(parent) {                            new_parents.extend(parents.iter().cloned());                        } else {                            new_parents.push(parent.clone());                        }                    }                    rewriter.set_new_rewritten_parents(&new_parents);                }                if rewriter.parents_changed() {                    let builder = rewriter.rebase().await?;                    builder.write().await?;                }                Ok(())            },        )        .await?;     tx.finish(ui, format!("parallelize {} commits", target_commits.len()))        .await}