just playing with tangled
at ig/vimdiffwarn 147 lines 5.8 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::ArgValueCandidates; 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 = ArgValueCandidates::new(complete::mutable_revisions) 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 workspace_command.check_rewritable(target_commits.iter().ids())?; 81 82 let mut tx = workspace_command.start_transaction(); 83 84 // New parents for commits in the target set. Since commits in the set are now 85 // supposed to be independent, they inherit the parent's non-target parents, 86 // recursively. 87 let mut new_target_parents: HashMap<CommitId, Vec<CommitId>> = HashMap::new(); 88 for commit in target_commits.iter().rev() { 89 let mut new_parents = vec![]; 90 for old_parent in commit.parent_ids() { 91 if let Some(grand_parents) = new_target_parents.get(old_parent) { 92 new_parents.extend_from_slice(grand_parents); 93 } else { 94 new_parents.push(old_parent.clone()); 95 } 96 } 97 new_target_parents.insert(commit.id().clone(), new_parents); 98 } 99 100 // If a commit outside the target set has a commit in the target set as parent, 101 // then - after the transformation - it should also have that commit's 102 // parents as direct parents, if those commits are also in the target set. 103 let mut new_child_parents: HashMap<CommitId, IndexSet<CommitId>> = HashMap::new(); 104 for commit in target_commits.iter().rev() { 105 let mut new_parents = IndexSet::new(); 106 for old_parent in commit.parent_ids() { 107 if let Some(parents) = new_child_parents.get(old_parent) { 108 new_parents.extend(parents.iter().cloned()); 109 } 110 } 111 new_parents.insert(commit.id().clone()); 112 new_child_parents.insert(commit.id().clone(), new_parents); 113 } 114 115 tx.repo_mut().transform_descendants( 116 target_commits.iter().ids().cloned().collect_vec(), 117 |mut rewriter| { 118 // Commits in the target set do not depend on each other but they still depend 119 // on other parents 120 if let Some(new_parents) = new_target_parents.get(rewriter.old_commit().id()) { 121 rewriter.set_new_rewritten_parents(new_parents); 122 } else if rewriter 123 .old_commit() 124 .parent_ids() 125 .iter() 126 .any(|id| new_child_parents.contains_key(id)) 127 { 128 let mut new_parents = vec![]; 129 for parent in rewriter.old_commit().parent_ids() { 130 if let Some(parents) = new_child_parents.get(parent) { 131 new_parents.extend(parents.iter().cloned()); 132 } else { 133 new_parents.push(parent.clone()); 134 } 135 } 136 rewriter.set_new_rewritten_parents(&new_parents); 137 } 138 if rewriter.parents_changed() { 139 let builder = rewriter.rebase()?; 140 builder.write()?; 141 } 142 Ok(()) 143 }, 144 )?; 145 146 tx.finish(ui, format!("parallelize {} commits", target_commits.len())) 147}