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 indexmap::IndexSet;
18use itertools::Itertools;
19use jj_lib::backend::CommitId;
20use jj_lib::commit::{Commit, CommitIteratorExt};
21use tracing::instrument;
22
23use crate::cli_util::{CommandHelper, RevisionArg};
24use crate::command_error::CommandError;
25use crate::ui::Ui;
26
27/// Parallelize revisions by making them siblings
28///
29/// Running `jj parallelize 1::2` will transform the history like this:
30/// ```text
31/// 3
32/// | 3
33/// 2 / \
34/// | -> 1 2
35/// 1 \ /
36/// | 0
37/// 0
38/// ```
39///
40/// The command effectively says "these revisions are actually independent",
41/// meaning that they should no longer be ancestors/descendants of each other.
42/// However, revisions outside the set that were previously ancestors of a
43/// revision in the set will remain ancestors of it. For example, revision 0
44/// above remains an ancestor of both 1 and 2. Similarly,
45/// revisions outside the set that were previously descendants of a revision
46/// in the set will remain descendants of it. For example, revision 3 above
47/// remains a descendant of both 1 and 2.
48///
49/// Therefore, `jj parallelize '1 | 3'` is a no-op. That's because 2, which is
50/// not in the target set, was a descendant of 1 before, so it remains a
51/// descendant, and it was an ancestor of 3 before, so it remains an ancestor.
52#[derive(clap::Args, Clone, Debug)]
53#[command(verbatim_doc_comment)]
54pub(crate) struct ParallelizeArgs {
55 /// Revisions to parallelize
56 revisions: Vec<RevisionArg>,
57}
58
59#[instrument(skip_all)]
60pub(crate) fn cmd_parallelize(
61 ui: &mut Ui,
62 command: &CommandHelper,
63 args: &ParallelizeArgs,
64) -> Result<(), CommandError> {
65 let mut workspace_command = command.workspace_helper(ui)?;
66 // The target commits are the commits being parallelized. They are ordered
67 // here with children before parents.
68 let target_commits: Vec<Commit> = workspace_command
69 .parse_union_revsets(&args.revisions)?
70 .evaluate_to_commits()?
71 .try_collect()?;
72 workspace_command.check_rewritable(target_commits.iter().ids())?;
73
74 let mut tx = workspace_command.start_transaction();
75
76 // New parents for commits in the target set. Since commits in the set are now
77 // supposed to be independent, they inherit the parent's non-target parents,
78 // recursively.
79 let mut new_target_parents: HashMap<CommitId, Vec<CommitId>> = HashMap::new();
80 for commit in target_commits.iter().rev() {
81 let mut new_parents = vec![];
82 for old_parent in commit.parent_ids() {
83 if let Some(grand_parents) = new_target_parents.get(old_parent) {
84 new_parents.extend_from_slice(grand_parents);
85 } else {
86 new_parents.push(old_parent.clone());
87 }
88 }
89 new_target_parents.insert(commit.id().clone(), new_parents);
90 }
91
92 // If a commit outside the target set has a commit in the target set as parent,
93 // then - after the transformation - it should also have that commit's
94 // parents as direct parents, if those commits are also in the target set.
95 let mut new_child_parents: HashMap<CommitId, IndexSet<CommitId>> = HashMap::new();
96 for commit in target_commits.iter().rev() {
97 let mut new_parents = IndexSet::new();
98 for old_parent in commit.parent_ids() {
99 if let Some(parents) = new_child_parents.get(old_parent) {
100 new_parents.extend(parents.iter().cloned());
101 }
102 }
103 new_parents.insert(commit.id().clone());
104 new_child_parents.insert(commit.id().clone(), new_parents);
105 }
106
107 tx.mut_repo().transform_descendants(
108 command.settings(),
109 target_commits.iter().ids().cloned().collect_vec(),
110 |mut rewriter| {
111 // Commits in the target set do not depend on each other but they still depend
112 // on other parents
113 if let Some(new_parents) = new_target_parents.get(rewriter.old_commit().id()) {
114 rewriter.set_new_rewritten_parents(new_parents.clone());
115 } else if rewriter
116 .old_commit()
117 .parent_ids()
118 .iter()
119 .any(|id| new_child_parents.contains_key(id))
120 {
121 let mut new_parents = vec![];
122 for parent in rewriter.old_commit().parent_ids() {
123 if let Some(parents) = new_child_parents.get(parent) {
124 new_parents.extend(parents.iter().cloned());
125 } else {
126 new_parents.push(parent.clone());
127 }
128 }
129 rewriter.set_new_rewritten_parents(new_parents);
130 }
131 if rewriter.parents_changed() {
132 let builder = rewriter.rebase(command.settings())?;
133 builder.write()?;
134 }
135 Ok(())
136 },
137 )?;
138
139 tx.finish(ui, format!("parallelize {} commits", target_commits.len()))
140}