just playing with tangled
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}