jujutsu_lib/
rewrite.rs

1// Copyright 2020 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, HashSet};
16
17use itertools::{process_results, Itertools};
18
19use crate::backend::{BackendError, BackendResult, CommitId, ObjectId};
20use crate::commit::Commit;
21use crate::dag_walk;
22use crate::op_store::RefTarget;
23use crate::repo::{MutableRepo, Repo};
24use crate::repo_path::RepoPath;
25use crate::revset::RevsetExpression;
26use crate::settings::UserSettings;
27use crate::tree::{merge_trees, Tree};
28use crate::view::RefName;
29
30pub fn merge_commit_trees(repo: &dyn Repo, commits: &[Commit]) -> Tree {
31    let store = repo.store();
32    if commits.is_empty() {
33        store
34            .get_tree(&RepoPath::root(), store.empty_tree_id())
35            .unwrap()
36    } else {
37        let index = repo.index();
38        let mut new_tree = commits[0].tree();
39        let commit_ids = commits
40            .iter()
41            .map(|commit| commit.id().clone())
42            .collect_vec();
43        for (i, other_commit) in commits.iter().enumerate().skip(1) {
44            let ancestor_ids = index.common_ancestors(&commit_ids[0..i], &[commit_ids[i].clone()]);
45            let ancestors = ancestor_ids
46                .iter()
47                .map(|id| store.get_commit(id).unwrap())
48                .collect_vec();
49            let ancestor_tree = merge_commit_trees(repo, &ancestors);
50            let new_tree_id = merge_trees(&new_tree, &ancestor_tree, &other_commit.tree()).unwrap();
51            new_tree = store.get_tree(&RepoPath::root(), &new_tree_id).unwrap();
52        }
53        new_tree
54    }
55}
56
57pub fn rebase_commit(
58    settings: &UserSettings,
59    mut_repo: &mut MutableRepo,
60    old_commit: &Commit,
61    new_parents: &[Commit],
62) -> BackendResult<Commit> {
63    let old_parents = old_commit.parents();
64    let old_parent_trees = old_parents
65        .iter()
66        .map(|parent| parent.store_commit().root_tree.clone())
67        .collect_vec();
68    let new_parent_trees = new_parents
69        .iter()
70        .map(|parent| parent.store_commit().root_tree.clone())
71        .collect_vec();
72    let new_tree_id = if new_parent_trees == old_parent_trees {
73        // Optimization
74        old_commit.tree_id().clone()
75    } else {
76        let old_base_tree = merge_commit_trees(mut_repo, &old_parents);
77        let new_base_tree = merge_commit_trees(mut_repo, new_parents);
78        // TODO: pass in labels for the merge parts
79        merge_trees(&new_base_tree, &old_base_tree, &old_commit.tree()).unwrap()
80    };
81    let new_parent_ids = new_parents
82        .iter()
83        .map(|commit| commit.id().clone())
84        .collect();
85    mut_repo
86        .rewrite_commit(settings, old_commit)
87        .set_parents(new_parent_ids)
88        .set_tree(new_tree_id)
89        .write()
90}
91
92pub fn back_out_commit(
93    settings: &UserSettings,
94    mut_repo: &mut MutableRepo,
95    old_commit: &Commit,
96    new_parents: &[Commit],
97) -> BackendResult<Commit> {
98    let old_base_tree = merge_commit_trees(mut_repo, &old_commit.parents());
99    let new_base_tree = merge_commit_trees(mut_repo, new_parents);
100    // TODO: pass in labels for the merge parts
101    let new_tree_id = merge_trees(&new_base_tree, &old_commit.tree(), &old_base_tree).unwrap();
102    let new_parent_ids = new_parents
103        .iter()
104        .map(|commit| commit.id().clone())
105        .collect();
106    // TODO: i18n the description based on repo language
107    mut_repo
108        .new_commit(settings, new_parent_ids, new_tree_id)
109        .set_description(format!("backout of commit {}", &old_commit.id().hex()))
110        .write()
111}
112
113/// Rebases descendants of a commit onto a new commit (or several).
114// TODO: Should there be an option to drop empty commits (and/or an option to
115// drop empty commits only if they weren't already empty)? Or maybe that
116// shouldn't be this type's job.
117pub struct DescendantRebaser<'settings, 'repo> {
118    settings: &'settings UserSettings,
119    mut_repo: &'repo mut MutableRepo,
120    // The commit identified by the key has been replaced by all the ones in the value, typically
121    // because the key commit was abandoned (the value commits are then the abandoned commit's
122    // parents). A child of the key commit should be rebased onto all the value commits. A branch
123    // pointing to the key commit should become a conflict pointing to all the value commits.
124    new_parents: HashMap<CommitId, Vec<CommitId>>,
125    divergent: HashMap<CommitId, Vec<CommitId>>,
126    // In reverse order (parents after children), so we can remove the last one to rebase first.
127    to_visit: Vec<CommitId>,
128    // Commits to visit but skip. These were also in `to_visit` to start with, but we don't
129    // want to rebase them. Instead, we record them in `replacements` when we visit them. That way,
130    // their descendants will be rebased correctly.
131    abandoned: HashSet<CommitId>,
132    new_commits: HashSet<CommitId>,
133    rebased: HashMap<CommitId, CommitId>,
134    // Names of branches where local target includes the commit id in the key.
135    branches: HashMap<CommitId, HashSet<String>>,
136    // Parents of rebased/abandoned commit that should become new heads once their descendants
137    // have been rebased.
138    heads_to_add: HashSet<CommitId>,
139    heads_to_remove: Vec<CommitId>,
140}
141
142impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
143    pub fn new(
144        settings: &'settings UserSettings,
145        mut_repo: &'repo mut MutableRepo,
146        rewritten: HashMap<CommitId, HashSet<CommitId>>,
147        abandoned: HashSet<CommitId>,
148    ) -> DescendantRebaser<'settings, 'repo> {
149        let root_commit_id = mut_repo.store().root_commit_id();
150        assert!(!abandoned.contains(root_commit_id));
151        assert!(!rewritten.contains_key(root_commit_id));
152        let old_commits_expression = RevsetExpression::commits(rewritten.keys().cloned().collect())
153            .union(&RevsetExpression::commits(
154                abandoned.iter().cloned().collect(),
155            ));
156        let heads_to_add_expression = old_commits_expression
157            .parents()
158            .minus(&old_commits_expression);
159        let heads_to_add = heads_to_add_expression
160            .evaluate(mut_repo, None)
161            .unwrap()
162            .iter()
163            .commit_ids()
164            .collect();
165
166        let to_visit_expression = old_commits_expression.descendants();
167        let to_visit_revset = to_visit_expression.evaluate(mut_repo, None).unwrap();
168        let to_visit_entries = to_visit_revset.iter().collect_vec();
169        drop(to_visit_revset);
170        let index = mut_repo.index();
171        let to_visit_set: HashSet<CommitId> = to_visit_entries
172            .iter()
173            .map(|entry| entry.commit_id())
174            .collect();
175        let mut visited = HashSet::new();
176        // Calculate an order where we rebase parents first, but if the parents were
177        // rewritten, make sure we rebase the rewritten parent first.
178        let to_visit = dag_walk::topo_order_reverse(
179            to_visit_entries,
180            Box::new(|entry| entry.commit_id()),
181            Box::new(|entry| {
182                visited.insert(entry.commit_id());
183                let mut dependents = vec![];
184                for parent in entry.parents() {
185                    if let Some(targets) = rewritten.get(&parent.commit_id()) {
186                        for target in targets {
187                            if to_visit_set.contains(target) && !visited.contains(target) {
188                                dependents.push(index.entry_by_id(target).unwrap());
189                            }
190                        }
191                    }
192                    if to_visit_set.contains(&parent.commit_id()) {
193                        dependents.push(parent);
194                    }
195                }
196                dependents
197            }),
198        );
199        let to_visit = to_visit.iter().map(|entry| entry.commit_id()).collect_vec();
200
201        let new_commits = rewritten.values().flatten().cloned().collect();
202
203        let mut new_parents = HashMap::new();
204        let mut divergent = HashMap::new();
205        for (old_commit, new_commits) in rewritten {
206            if new_commits.len() == 1 {
207                new_parents.insert(old_commit, vec![new_commits.iter().next().unwrap().clone()]);
208            } else {
209                // The call to index.heads() is mostly to get a predictable order
210                let new_commits = mut_repo.index().heads(&mut new_commits.iter());
211                divergent.insert(old_commit, new_commits);
212            }
213        }
214
215        // Build a map from commit to branches pointing to it, so we don't need to scan
216        // all branches each time we rebase a commit.
217        let mut branches: HashMap<_, HashSet<_>> = HashMap::new();
218        for (branch_name, branch_target) in mut_repo.view().branches() {
219            if let Some(local_target) = &branch_target.local_target {
220                for commit in local_target.adds() {
221                    branches
222                        .entry(commit)
223                        .or_default()
224                        .insert(branch_name.clone());
225                }
226                for commit in local_target.removes() {
227                    branches
228                        .entry(commit)
229                        .or_default()
230                        .insert(branch_name.clone());
231                }
232            }
233        }
234
235        DescendantRebaser {
236            settings,
237            mut_repo,
238            new_parents,
239            divergent,
240            to_visit,
241            abandoned,
242            new_commits,
243            rebased: Default::default(),
244            branches,
245            heads_to_add,
246            heads_to_remove: Default::default(),
247        }
248    }
249
250    /// Returns a map from `CommitId` of old commit to new commit. Includes the
251    /// commits rebase so far. Does not include the inputs passed to
252    /// `rebase_descendants`.
253    pub fn rebased(&self) -> &HashMap<CommitId, CommitId> {
254        &self.rebased
255    }
256
257    fn new_parents(&self, old_ids: &[CommitId]) -> Vec<CommitId> {
258        let mut new_ids = vec![];
259        for old_id in old_ids {
260            if let Some(new_parent_ids) = self.new_parents.get(old_id) {
261                for new_parent_id in new_parent_ids {
262                    // The new parent may itself have been rebased earlier in the process
263                    if let Some(newer_parent_id) = self.rebased.get(new_parent_id) {
264                        new_ids.push(newer_parent_id.clone());
265                    } else {
266                        new_ids.push(new_parent_id.clone());
267                    }
268                }
269            } else if let Some(new_parent_id) = self.rebased.get(old_id) {
270                new_ids.push(new_parent_id.clone());
271            } else {
272                new_ids.push(old_id.clone());
273            };
274        }
275        new_ids
276    }
277
278    fn ref_target_update(old_id: CommitId, new_ids: Vec<CommitId>) -> (RefTarget, RefTarget) {
279        let old_ids = std::iter::repeat(old_id).take(new_ids.len()).collect_vec();
280        (
281            RefTarget::Conflict {
282                removes: vec![],
283                adds: old_ids,
284            },
285            RefTarget::Conflict {
286                removes: vec![],
287                adds: new_ids,
288            },
289        )
290    }
291
292    fn update_references(
293        &mut self,
294        old_commit_id: CommitId,
295        new_commit_ids: Vec<CommitId>,
296        edit: bool,
297    ) -> Result<(), BackendError> {
298        // We arbitrarily pick a new working-copy commit among the candidates.
299        self.update_wc_commits(&old_commit_id, &new_commit_ids[0], edit)?;
300
301        if let Some(branch_names) = self.branches.get(&old_commit_id).cloned() {
302            let mut branch_updates = vec![];
303            for branch_name in &branch_names {
304                for new_commit_id in &new_commit_ids {
305                    self.branches
306                        .entry(new_commit_id.clone())
307                        .or_default()
308                        .insert(branch_name.clone());
309                }
310                let local_target = self.mut_repo.get_local_branch(branch_name).unwrap();
311                for old_add in local_target.adds() {
312                    if old_add == old_commit_id {
313                        branch_updates.push(branch_name.clone());
314                    }
315                }
316            }
317            let (old_target, new_target) =
318                DescendantRebaser::ref_target_update(old_commit_id.clone(), new_commit_ids);
319            for branch_name in branch_updates {
320                self.mut_repo.merge_single_ref(
321                    &RefName::LocalBranch(branch_name),
322                    Some(&old_target),
323                    Some(&new_target),
324                );
325            }
326        }
327
328        self.heads_to_add.remove(&old_commit_id);
329        if !self.new_commits.contains(&old_commit_id) || self.rebased.contains_key(&old_commit_id) {
330            self.heads_to_remove.push(old_commit_id);
331        }
332        Ok(())
333    }
334
335    fn update_wc_commits(
336        &mut self,
337        old_commit_id: &CommitId,
338        new_commit_id: &CommitId,
339        edit: bool,
340    ) -> Result<(), BackendError> {
341        let workspaces_to_update = self
342            .mut_repo
343            .view()
344            .workspaces_for_wc_commit_id(old_commit_id);
345        if workspaces_to_update.is_empty() {
346            return Ok(());
347        }
348
349        let new_commit = self.mut_repo.store().get_commit(new_commit_id)?;
350        let new_wc_commit = if edit {
351            new_commit
352        } else {
353            self.mut_repo
354                .new_commit(
355                    self.settings,
356                    vec![new_commit.id().clone()],
357                    new_commit.tree_id().clone(),
358                )
359                .write()?
360        };
361        for workspace_id in workspaces_to_update.into_iter() {
362            self.mut_repo.edit(workspace_id, &new_wc_commit).unwrap();
363        }
364        Ok(())
365    }
366
367    // TODO: Perhaps change the interface since it's not just about rebasing
368    // commits.
369    pub fn rebase_next(&mut self) -> Result<Option<RebasedDescendant>, BackendError> {
370        while let Some(old_commit_id) = self.to_visit.pop() {
371            if let Some(new_parent_ids) = self.new_parents.get(&old_commit_id).cloned() {
372                // This is a commit that had already been rebased before `self` was created
373                // (i.e. it's part of the input for this rebase). We don't need
374                // to rebase it, but we still want to update branches pointing
375                // to the old commit.
376                self.update_references(old_commit_id, new_parent_ids, true)?;
377                continue;
378            }
379            if let Some(divergent_ids) = self.divergent.get(&old_commit_id).cloned() {
380                // Leave divergent commits in place. Don't update `new_parents` since we don't
381                // want to rebase descendants either.
382                self.update_references(old_commit_id, divergent_ids, true)?;
383                continue;
384            }
385            let old_commit = self.mut_repo.store().get_commit(&old_commit_id)?;
386            let old_parent_ids = old_commit.parent_ids();
387            let new_parent_ids = self.new_parents(old_parent_ids);
388            if self.abandoned.contains(&old_commit_id) {
389                // Update the `new_parents` map so descendants are rebased correctly.
390                self.new_parents
391                    .insert(old_commit_id.clone(), new_parent_ids.clone());
392                self.update_references(old_commit_id, new_parent_ids, false)?;
393                continue;
394            } else if new_parent_ids == old_parent_ids {
395                // The commit is already in place.
396                continue;
397            }
398
399            // Don't create commit where one parent is an ancestor of another.
400            let head_set: HashSet<_> = self
401                .mut_repo
402                .index()
403                .heads(&mut new_parent_ids.iter())
404                .iter()
405                .cloned()
406                .collect();
407            let new_parents = process_results(
408                new_parent_ids
409                    .iter()
410                    .filter(|new_parent| head_set.contains(new_parent))
411                    .map(|new_parent_id| self.mut_repo.store().get_commit(new_parent_id)),
412                |iter| iter.collect_vec(),
413            )?;
414            let new_commit =
415                rebase_commit(self.settings, self.mut_repo, &old_commit, &new_parents)?;
416            self.rebased
417                .insert(old_commit_id.clone(), new_commit.id().clone());
418            self.update_references(old_commit_id, vec![new_commit.id().clone()], true)?;
419            return Ok(Some(RebasedDescendant {
420                old_commit,
421                new_commit,
422            }));
423        }
424        // TODO: As the TODO above says, we should probably change the API. Even if we
425        // don't, we should at least make this code not do any work if you call
426        // rebase_next() after we've returned None.
427        let mut view = self.mut_repo.view().store_view().clone();
428        for commit_id in &self.heads_to_remove {
429            view.head_ids.remove(commit_id);
430        }
431        for commit_id in &self.heads_to_add {
432            view.head_ids.insert(commit_id.clone());
433        }
434        self.heads_to_remove.clear();
435        self.heads_to_add.clear();
436        self.mut_repo.set_view(view);
437        self.mut_repo.clear_rewritten_commits();
438        self.mut_repo.clear_abandoned_commits();
439        Ok(None)
440    }
441
442    pub fn rebase_all(&mut self) -> Result<(), BackendError> {
443        while self.rebase_next()?.is_some() {}
444        Ok(())
445    }
446}
447
448#[derive(Debug, PartialEq, Eq, Clone)]
449pub struct RebasedDescendant {
450    pub old_commit: Commit,
451    pub new_commit: Commit,
452}