just playing with tangled
at main 149 lines 5.9 kB view raw
1// Copyright 2024 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::collections::HashMap; 16 17use clap_complete::ArgValueCompleter; 18use indexmap::IndexSet; 19use itertools::Itertools as _; 20use jj_lib::backend::CommitId; 21use jj_lib::commit::Commit; 22use jj_lib::commit::CommitIteratorExt as _; 23use tracing::instrument; 24 25use crate::cli_util::CommandHelper; 26use crate::cli_util::RevisionArg; 27use crate::command_error::CommandError; 28use crate::complete; 29use crate::ui::Ui; 30 31/// Parallelize revisions by making them siblings 32/// 33/// Running `jj parallelize 1::2` will transform the history like this: 34/// ```text 35/// 3 36/// | 3 37/// 2 / \ 38/// | -> 1 2 39/// 1 \ / 40/// | 0 41/// 0 42/// ``` 43/// 44/// The command effectively says "these revisions are actually independent", 45/// meaning that they should no longer be ancestors/descendants of each other. 46/// However, revisions outside the set that were previously ancestors of a 47/// revision in the set will remain ancestors of it. For example, revision 0 48/// above remains an ancestor of both 1 and 2. Similarly, 49/// revisions outside the set that were previously descendants of a revision 50/// in the set will remain descendants of it. For example, revision 3 above 51/// remains a descendant of both 1 and 2. 52/// 53/// Therefore, `jj parallelize '1 | 3'` is a no-op. That's because 2, which is 54/// not in the target set, was a descendant of 1 before, so it remains a 55/// descendant, and it was an ancestor of 3 before, so it remains an ancestor. 56#[derive(clap::Args, Clone, Debug)] 57#[command(verbatim_doc_comment)] 58pub(crate) struct ParallelizeArgs { 59 /// Revisions to parallelize 60 #[arg( 61 value_name = "REVSETS", 62 add = ArgValueCompleter::new(complete::revset_expression_mutable), 63 )] 64 revisions: Vec<RevisionArg>, 65} 66 67#[instrument(skip_all)] 68pub(crate) fn cmd_parallelize( 69 ui: &mut Ui, 70 command: &CommandHelper, 71 args: &ParallelizeArgs, 72) -> Result<(), CommandError> { 73 let mut workspace_command = command.workspace_helper(ui)?; 74 // The target commits are the commits being parallelized. They are ordered 75 // here with children before parents. 76 let target_commits: Vec<Commit> = workspace_command 77 .parse_union_revsets(ui, &args.revisions)? 78 .evaluate_to_commits()? 79 .try_collect()?; 80 81 // New parents for commits in the target set. Since commits in the set are now 82 // supposed to be independent, they inherit the parent's non-target parents, 83 // recursively. 84 let mut new_target_parents: HashMap<CommitId, Vec<CommitId>> = HashMap::new(); 85 let mut needs_rewrite = Vec::new(); 86 for commit in target_commits.iter().rev() { 87 let mut new_parents = vec![]; 88 for old_parent in commit.parent_ids() { 89 if let Some(grand_parents) = new_target_parents.get(old_parent) { 90 new_parents.extend_from_slice(grand_parents); 91 needs_rewrite.push(commit.id()); 92 } else { 93 new_parents.push(old_parent.clone()); 94 } 95 } 96 new_target_parents.insert(commit.id().clone(), new_parents); 97 } 98 99 workspace_command.check_rewritable(needs_rewrite)?; 100 let mut tx = workspace_command.start_transaction(); 101 102 // If a commit outside the target set has a commit in the target set as parent, 103 // then - after the transformation - it should also have that commit's 104 // parents as direct parents, if those commits are also in the target set. 105 let mut new_child_parents: HashMap<CommitId, IndexSet<CommitId>> = HashMap::new(); 106 for commit in target_commits.iter().rev() { 107 let mut new_parents = IndexSet::new(); 108 for old_parent in commit.parent_ids() { 109 if let Some(parents) = new_child_parents.get(old_parent) { 110 new_parents.extend(parents.iter().cloned()); 111 } 112 } 113 new_parents.insert(commit.id().clone()); 114 new_child_parents.insert(commit.id().clone(), new_parents); 115 } 116 117 tx.repo_mut().transform_descendants( 118 target_commits.iter().ids().cloned().collect_vec(), 119 |mut rewriter| { 120 // Commits in the target set do not depend on each other but they still depend 121 // on other parents 122 if let Some(new_parents) = new_target_parents.get(rewriter.old_commit().id()) { 123 rewriter.set_new_rewritten_parents(new_parents); 124 } else if rewriter 125 .old_commit() 126 .parent_ids() 127 .iter() 128 .any(|id| new_child_parents.contains_key(id)) 129 { 130 let mut new_parents = vec![]; 131 for parent in rewriter.old_commit().parent_ids() { 132 if let Some(parents) = new_child_parents.get(parent) { 133 new_parents.extend(parents.iter().cloned()); 134 } else { 135 new_parents.push(parent.clone()); 136 } 137 } 138 rewriter.set_new_rewritten_parents(&new_parents); 139 } 140 if rewriter.parents_changed() { 141 let builder = rewriter.rebase()?; 142 builder.write()?; 143 } 144 Ok(()) 145 }, 146 )?; 147 148 tx.finish(ui, format!("parallelize {} commits", target_commits.len())) 149}