Skip to main content

jj_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
15#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::collections::HashSet;
19use std::slice;
20use std::sync::Arc;
21
22use futures::future::try_join_all;
23use futures::try_join;
24use indexmap::IndexMap;
25use indexmap::IndexSet;
26use itertools::Itertools as _;
27use pollster::FutureExt as _;
28use tracing::instrument;
29
30use crate::backend::BackendError;
31use crate::backend::BackendResult;
32use crate::backend::CommitId;
33use crate::commit::Commit;
34use crate::commit::CommitIteratorExt as _;
35use crate::commit::conflict_label_for_commits;
36use crate::commit_builder::CommitBuilder;
37use crate::conflict_labels::ConflictLabels;
38use crate::index::Index;
39use crate::index::IndexResult;
40use crate::index::ResolvedChangeTargets;
41use crate::iter_util::fallible_any;
42use crate::matchers::Matcher;
43use crate::matchers::Visit;
44use crate::merge::Diff;
45use crate::merge::Merge;
46use crate::merged_tree::MergedTree;
47use crate::merged_tree_builder::MergedTreeBuilder;
48use crate::repo::MutableRepo;
49use crate::repo::Repo;
50use crate::repo_path::RepoPath;
51use crate::revset::RevsetExpression;
52use crate::revset::RevsetIteratorExt as _;
53use crate::store::Store;
54
55/// Merges `commits` and tries to resolve any conflicts recursively.
56#[instrument(skip(repo))]
57pub async fn merge_commit_trees(repo: &dyn Repo, commits: &[Commit]) -> BackendResult<MergedTree> {
58    if let [commit] = commits {
59        Ok(commit.tree())
60    } else {
61        merge_commit_trees_no_resolve_without_repo(repo.store(), repo.index(), commits)
62            .await?
63            .resolve()
64            .await
65    }
66}
67
68/// Merges `commits` without attempting to resolve file conflicts.
69#[instrument(skip(index))]
70pub async fn merge_commit_trees_no_resolve_without_repo(
71    store: &Arc<Store>,
72    index: &dyn Index,
73    commits: &[Commit],
74) -> BackendResult<MergedTree> {
75    let commit_ids = commits
76        .iter()
77        .map(|commit| commit.id().clone())
78        .collect_vec();
79    let commit_id_merge = find_recursive_merge_commits(store, index, commit_ids)?;
80    let tree_merge: Merge<(MergedTree, String)> = commit_id_merge
81        .try_map_async(async |commit_id| {
82            let commit = store.get_commit_async(commit_id).await?;
83            Ok::<_, BackendError>((commit.tree(), commit.conflict_label()))
84        })
85        .await?;
86    Ok(MergedTree::merge_no_resolve(tree_merge))
87}
88
89/// Find the commits to use as input to the recursive merge algorithm.
90pub fn find_recursive_merge_commits(
91    store: &Arc<Store>,
92    index: &dyn Index,
93    mut commit_ids: Vec<CommitId>,
94) -> BackendResult<Merge<CommitId>> {
95    if commit_ids.is_empty() {
96        Ok(Merge::resolved(store.root_commit_id().clone()))
97    } else if commit_ids.len() == 1 {
98        Ok(Merge::resolved(commit_ids.pop().unwrap()))
99    } else {
100        let mut result = Merge::resolved(commit_ids[0].clone());
101        for (i, other_commit_id) in commit_ids.iter().enumerate().skip(1) {
102            let ancestor_ids = index
103                .common_ancestors(&commit_ids[0..i], &commit_ids[i..][..1])
104                // TODO: indexing error shouldn't be a "BackendError"
105                .map_err(|err| BackendError::Other(err.into()))?;
106            let ancestor_merge = find_recursive_merge_commits(store, index, ancestor_ids)?;
107            result = Merge::from_vec(vec![
108                result,
109                ancestor_merge,
110                Merge::resolved(other_commit_id.clone()),
111            ])
112            .flatten();
113        }
114        Ok(result)
115    }
116}
117
118/// Restore matching paths from the source into the destination.
119pub async fn restore_tree(
120    source: &MergedTree,
121    destination: &MergedTree,
122    source_label: String,
123    destination_label: String,
124    matcher: &dyn Matcher,
125) -> BackendResult<MergedTree> {
126    if matcher.visit(RepoPath::root()) == Visit::AllRecursively {
127        // Optimization for a common case
128        return Ok(source.clone());
129    }
130
131    let select_matching =
132        |tree: &MergedTree, labels: ConflictLabels| -> BackendResult<MergedTree> {
133            let empty_tree_ids = Merge::repeated(
134                tree.store().empty_tree_id().clone(),
135                tree.tree_ids().num_sides(),
136            );
137            let labeled_empty_tree = MergedTree::new(tree.store().clone(), empty_tree_ids, labels);
138            // TODO: We should be able to not traverse deeper in the diff if the matcher
139            // matches an entire subtree.
140            let mut builder = MergedTreeBuilder::new(labeled_empty_tree);
141            for (path, value) in tree.entries_matching(matcher) {
142                // TODO: if https://github.com/jj-vcs/jj/issues/4152 is implemented, we will need
143                // to expand resolved conflicts into `Merge::repeated(value, num_sides)`.
144                builder.set_or_remove(path, value?);
145            }
146            builder.write_tree()
147        };
148
149    const RESTORE_BASE_LABEL: &str = "base files for restore";
150
151    // To avoid confusion between the destination tree and the base tree, we add a
152    // prefix to the conflict labels of the base tree.
153    let base_labels = ConflictLabels::from_merge(destination.labels().as_merge().map(|label| {
154        if label.is_empty() || label.starts_with(RESTORE_BASE_LABEL) {
155            label.clone()
156        } else {
157            format!("{RESTORE_BASE_LABEL} (from {label})")
158        }
159    }));
160
161    // Merging the trees this way ensures that when restoring a conflicted file into
162    // a conflicted commit, we preserve the labels of both commits even if the
163    // commits had different conflict labels. The labels we add here for
164    // non-conflicted trees will generally not be visible to users since they will
165    // always be removed during simplification when materializing any individual
166    // file. However, they could be useful in the future if we add a command which
167    // shows the labels for all the sides of a conflicted commit, and they are also
168    // useful for debugging.
169    // TODO: using a merge is required for retaining conflict labels when restoring
170    // from/into conflicted trees, but maybe we could optimize the case where both
171    // trees are already resolved.
172    MergedTree::merge(Merge::from_vec(vec![
173        (
174            destination.clone(),
175            format!("{destination_label} (restore destination)"),
176        ),
177        (
178            select_matching(destination, base_labels)?,
179            format!("{RESTORE_BASE_LABEL} (from {destination_label})"),
180        ),
181        (
182            select_matching(source, source.labels().clone())?,
183            format!("restored files (from {source_label})"),
184        ),
185    ]))
186    .await
187}
188
189pub async fn rebase_commit(
190    mut_repo: &mut MutableRepo,
191    old_commit: Commit,
192    new_parents: Vec<CommitId>,
193) -> BackendResult<Commit> {
194    let rewriter = CommitRewriter::new(mut_repo, old_commit, new_parents);
195    let builder = rewriter.rebase().await?;
196    builder.write()
197}
198
199/// Helps rewrite a commit.
200pub struct CommitRewriter<'repo> {
201    mut_repo: &'repo mut MutableRepo,
202    old_commit: Commit,
203    new_parents: Vec<CommitId>,
204}
205
206impl<'repo> CommitRewriter<'repo> {
207    /// Create a new instance.
208    pub fn new(
209        mut_repo: &'repo mut MutableRepo,
210        old_commit: Commit,
211        new_parents: Vec<CommitId>,
212    ) -> Self {
213        Self {
214            mut_repo,
215            old_commit,
216            new_parents,
217        }
218    }
219
220    /// Returns the `MutableRepo`.
221    pub fn repo_mut(&mut self) -> &mut MutableRepo {
222        self.mut_repo
223    }
224
225    /// The commit we're rewriting.
226    pub fn old_commit(&self) -> &Commit {
227        &self.old_commit
228    }
229
230    /// Get the old commit's intended new parents.
231    pub fn new_parents(&self) -> &[CommitId] {
232        &self.new_parents
233    }
234
235    /// Set the old commit's intended new parents.
236    pub fn set_new_parents(&mut self, new_parents: Vec<CommitId>) {
237        self.new_parents = new_parents;
238    }
239
240    /// Set the old commit's intended new parents to be the rewritten versions
241    /// of the given parents.
242    pub fn set_new_rewritten_parents(&mut self, unrewritten_parents: &[CommitId]) {
243        self.new_parents = self.mut_repo.new_parents(unrewritten_parents);
244    }
245
246    /// Update the intended new parents by replacing any occurrence of
247    /// `old_parent` by `new_parents`.
248    pub fn replace_parent<'a>(
249        &mut self,
250        old_parent: &CommitId,
251        new_parents: impl IntoIterator<Item = &'a CommitId>,
252    ) {
253        if let Some(i) = self.new_parents.iter().position(|p| p == old_parent) {
254            self.new_parents
255                .splice(i..i + 1, new_parents.into_iter().cloned());
256            let mut unique = HashSet::new();
257            self.new_parents.retain(|p| unique.insert(p.clone()));
258        }
259    }
260
261    /// Checks if the intended new parents are different from the old commit's
262    /// parents.
263    pub fn parents_changed(&self) -> bool {
264        self.new_parents != self.old_commit.parent_ids()
265    }
266
267    /// If a merge commit would end up with one parent being an ancestor of the
268    /// other, then filter out the ancestor.
269    pub fn simplify_ancestor_merge(&mut self) -> IndexResult<()> {
270        let head_set: HashSet<_> = self
271            .mut_repo
272            .index()
273            .heads(&mut self.new_parents.iter())?
274            .into_iter()
275            .collect();
276        self.new_parents.retain(|parent| head_set.contains(parent));
277        Ok(())
278    }
279
280    /// Records the old commit as abandoned with the new parents.
281    ///
282    /// This is equivalent to `reparent(settings).abandon()`, but is cheaper.
283    pub fn abandon(self) {
284        let old_commit_id = self.old_commit.id().clone();
285        let new_parents = self.new_parents;
286        self.mut_repo
287            .record_abandoned_commit_with_parents(old_commit_id, new_parents);
288    }
289
290    /// Rebase the old commit onto the new parents. Returns a `CommitBuilder`
291    /// for the new commit. Returns `None` if the commit was abandoned.
292    pub async fn rebase_with_empty_behavior(
293        self,
294        empty: EmptyBehavior,
295    ) -> BackendResult<Option<CommitBuilder<'repo>>> {
296        let old_parents_fut = self.old_commit.parents_async();
297        let new_parents_fut = try_join_all(
298            self.new_parents
299                .iter()
300                .map(|new_parent_id| self.mut_repo.store().get_commit_async(new_parent_id)),
301        );
302        let (old_parents, new_parents) = try_join!(old_parents_fut, new_parents_fut)?;
303        let old_parent_trees = old_parents
304            .iter()
305            .map(|parent| parent.tree_ids().clone())
306            .collect_vec();
307        let new_parent_trees = new_parents
308            .iter()
309            .map(|parent| parent.tree_ids().clone())
310            .collect_vec();
311
312        let (was_empty, new_tree) = if new_parent_trees == old_parent_trees {
313            (
314                // Optimization: was_empty is only used for newly empty, but when the
315                // parents haven't changed it can't be newly empty.
316                true,
317                // Optimization: Skip merging.
318                self.old_commit.tree(),
319            )
320        } else {
321            // We wouldn't need to resolve merge conflicts here if the
322            // same-change rule is "keep". See 9d4a97381f30 "rewrite: don't
323            // resolve intermediate parent tree when rebasing" for details.
324            let old_base_tree_fut = merge_commit_trees(self.mut_repo, &old_parents);
325            let new_base_tree_fut = merge_commit_trees(self.mut_repo, &new_parents);
326            let old_tree = self.old_commit.tree();
327            let (old_base_tree, new_base_tree) = try_join!(old_base_tree_fut, new_base_tree_fut)?;
328            (
329                old_base_tree.tree_ids() == self.old_commit.tree_ids(),
330                MergedTree::merge(Merge::from_vec(vec![
331                    (
332                        new_base_tree,
333                        format!(
334                            "{} (rebase destination)",
335                            conflict_label_for_commits(&new_parents)
336                        ),
337                    ),
338                    (
339                        old_base_tree,
340                        format!(
341                            "{} (parents of rebased revision)",
342                            conflict_label_for_commits(&old_parents)
343                        ),
344                    ),
345                    (
346                        old_tree,
347                        format!("{} (rebased revision)", self.old_commit.conflict_label()),
348                    ),
349                ]))
350                .await?,
351            )
352        };
353        // Ensure we don't abandon commits with multiple parents (merge commits), even
354        // if they're empty.
355        if let [parent] = &new_parents[..] {
356            let should_abandon = match empty {
357                EmptyBehavior::Keep => false,
358                EmptyBehavior::AbandonNewlyEmpty => {
359                    parent.tree_ids() == new_tree.tree_ids() && !was_empty
360                }
361                EmptyBehavior::AbandonAllEmpty => parent.tree_ids() == new_tree.tree_ids(),
362            };
363            if should_abandon {
364                self.abandon();
365                return Ok(None);
366            }
367        }
368
369        let builder = self
370            .mut_repo
371            .rewrite_commit(&self.old_commit)
372            .set_parents(self.new_parents)
373            .set_tree(new_tree);
374        Ok(Some(builder))
375    }
376
377    /// Rebase the old commit onto the new parents. Returns a `CommitBuilder`
378    /// for the new commit.
379    pub async fn rebase(self) -> BackendResult<CommitBuilder<'repo>> {
380        let builder = self.rebase_with_empty_behavior(EmptyBehavior::Keep).await?;
381        Ok(builder.unwrap())
382    }
383
384    /// Rewrite the old commit onto the new parents without changing its
385    /// contents. Returns a `CommitBuilder` for the new commit.
386    pub fn reparent(self) -> CommitBuilder<'repo> {
387        self.mut_repo
388            .rewrite_commit(&self.old_commit)
389            .set_parents(self.new_parents)
390    }
391}
392
393#[derive(Debug)]
394pub enum RebasedCommit {
395    Rewritten(Commit),
396    Abandoned { parent_id: CommitId },
397}
398
399pub fn rebase_commit_with_options(
400    mut rewriter: CommitRewriter<'_>,
401    options: &RebaseOptions,
402) -> BackendResult<RebasedCommit> {
403    // If specified, don't create commit where one parent is an ancestor of another.
404    if options.simplify_ancestor_merge {
405        rewriter
406            .simplify_ancestor_merge()
407            // TODO: indexing error shouldn't be a "BackendError"
408            .map_err(|err| BackendError::Other(err.into()))?;
409    }
410
411    let single_parent = match &rewriter.new_parents[..] {
412        [parent_id] => Some(parent_id.clone()),
413        _ => None,
414    };
415    let new_parents_len = rewriter.new_parents.len();
416    if let Some(builder) = rewriter
417        .rebase_with_empty_behavior(options.empty)
418        .block_on()?
419    {
420        let new_commit = builder.write()?;
421        Ok(RebasedCommit::Rewritten(new_commit))
422    } else {
423        assert_eq!(new_parents_len, 1);
424        Ok(RebasedCommit::Abandoned {
425            parent_id: single_parent.unwrap(),
426        })
427    }
428}
429
430/// Moves changes from `sources` to the `destination` parent, returns new tree.
431// TODO: pass conflict labels as argument to provide more specific information
432pub fn rebase_to_dest_parent(
433    repo: &dyn Repo,
434    sources: &[Commit],
435    destination: &Commit,
436) -> BackendResult<MergedTree> {
437    if let [source] = sources
438        && source.parent_ids() == destination.parent_ids()
439    {
440        return Ok(source.tree());
441    }
442
443    let diffs: Vec<_> = sources
444        .iter()
445        .map(|source| -> BackendResult<_> {
446            Ok(Diff::new(
447                (
448                    source.parent_tree(repo)?,
449                    format!("{} (original parents)", source.parents_conflict_label()?),
450                ),
451                (
452                    source.tree(),
453                    format!("{} (original revision)", source.conflict_label()),
454                ),
455            ))
456        })
457        .try_collect()?;
458    MergedTree::merge(Merge::from_diffs(
459        (
460            destination.parent_tree(repo)?,
461            format!("{} (new parents)", destination.parents_conflict_label()?),
462        ),
463        diffs,
464    ))
465    .block_on()
466}
467
468#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
469pub enum EmptyBehavior {
470    /// Always keep empty commits
471    #[default]
472    Keep,
473    /// Skips commits that would be empty after the rebase, but that were not
474    /// originally empty.
475    /// Will never skip merge commits with multiple non-empty parents.
476    AbandonNewlyEmpty,
477    /// Skips all empty commits, including ones that were empty before the
478    /// rebase.
479    /// Will never skip merge commits with multiple non-empty parents.
480    AbandonAllEmpty,
481}
482
483/// Controls the configuration of a rebase.
484// If we wanted to add a flag similar to `git rebase --ignore-date`, then this
485// makes it much easier by ensuring that the only changes required are to
486// change the RebaseOptions construction in the CLI, and changing the
487// rebase_commit function to actually use the flag, and ensure we don't need to
488// plumb it in.
489#[derive(Clone, Debug, Default)]
490pub struct RebaseOptions {
491    pub empty: EmptyBehavior,
492    pub rewrite_refs: RewriteRefsOptions,
493    /// If a merge commit would end up with one parent being an ancestor of the
494    /// other, then filter out the ancestor.
495    pub simplify_ancestor_merge: bool,
496}
497
498/// Configuration for [`MutableRepo::update_rewritten_references()`].
499#[derive(Clone, Debug, Default)]
500pub struct RewriteRefsOptions {
501    /// Whether or not delete bookmarks pointing to the abandoned commits.
502    ///
503    /// If false, bookmarks will be moved to the parents of the abandoned
504    /// commit.
505    pub delete_abandoned_bookmarks: bool,
506}
507
508#[derive(Debug)]
509pub struct MoveCommitsStats {
510    /// The number of commits in the target set which were rebased.
511    pub num_rebased_targets: u32,
512    /// The number of descendant commits which were rebased.
513    pub num_rebased_descendants: u32,
514    /// The number of commits for which rebase was skipped, due to the commit
515    /// already being in place.
516    pub num_skipped_rebases: u32,
517    /// The number of commits which were abandoned due to being empty.
518    pub num_abandoned_empty: u32,
519    /// The rebased commits
520    pub rebased_commits: HashMap<CommitId, RebasedCommit>,
521}
522
523/// Target and destination commits to be rebased by [`move_commits()`].
524#[derive(Clone, Debug)]
525pub struct MoveCommitsLocation {
526    pub new_parent_ids: Vec<CommitId>,
527    pub new_child_ids: Vec<CommitId>,
528    pub target: MoveCommitsTarget,
529}
530
531#[derive(Clone, Debug)]
532pub enum MoveCommitsTarget {
533    /// The commits to be moved. Commits should be mutable and in reverse
534    /// topological order.
535    Commits(Vec<CommitId>),
536    /// The root commits to be moved, along with all their descendants.
537    Roots(Vec<CommitId>),
538}
539
540#[derive(Clone, Debug)]
541pub struct ComputedMoveCommits {
542    target_commit_ids: IndexSet<CommitId>,
543    descendants: Vec<Commit>,
544    commit_new_parents_map: HashMap<CommitId, Vec<CommitId>>,
545    to_abandon: HashSet<CommitId>,
546}
547
548impl ComputedMoveCommits {
549    fn empty() -> Self {
550        Self {
551            target_commit_ids: IndexSet::new(),
552            descendants: vec![],
553            commit_new_parents_map: HashMap::new(),
554            to_abandon: HashSet::new(),
555        }
556    }
557
558    /// Records a set of commits to abandon while rebasing.
559    ///
560    /// Abandoning these commits while rebasing ensures that their descendants
561    /// are still rebased properly. [`MutableRepo::record_abandoned_commit`] is
562    /// similar, but it can lead to issues when abandoning a target commit
563    /// before the rebase.
564    pub fn record_to_abandon(&mut self, commit_ids: impl IntoIterator<Item = CommitId>) {
565        self.to_abandon.extend(commit_ids);
566    }
567
568    pub fn apply(
569        self,
570        mut_repo: &mut MutableRepo,
571        options: &RebaseOptions,
572    ) -> BackendResult<MoveCommitsStats> {
573        apply_move_commits(mut_repo, self, options)
574    }
575}
576
577/// Moves `loc.target` commits from their current location to a new location in
578/// the graph.
579///
580/// Commits in `target` are rebased onto the new parents given by
581/// `new_parent_ids`, while the `new_child_ids` commits are rebased onto the
582/// heads of the commits in `targets`. This assumes that commits in `target` and
583/// `new_child_ids` can be rewritten, and there will be no cycles in the
584/// resulting graph. Commits in `target` should be in reverse topological order.
585pub fn move_commits(
586    mut_repo: &mut MutableRepo,
587    loc: &MoveCommitsLocation,
588    options: &RebaseOptions,
589) -> BackendResult<MoveCommitsStats> {
590    compute_move_commits(mut_repo, loc)?.apply(mut_repo, options)
591}
592
593pub fn compute_move_commits(
594    repo: &MutableRepo,
595    loc: &MoveCommitsLocation,
596) -> BackendResult<ComputedMoveCommits> {
597    let target_commit_ids: IndexSet<CommitId>;
598    let connected_target_commits: Vec<Commit>;
599    let connected_target_commits_internal_parents: HashMap<CommitId, IndexSet<CommitId>>;
600    let target_roots: HashSet<CommitId>;
601
602    match &loc.target {
603        MoveCommitsTarget::Commits(commit_ids) => {
604            if commit_ids.is_empty() {
605                return Ok(ComputedMoveCommits::empty());
606            }
607
608            target_commit_ids = commit_ids.iter().cloned().collect();
609
610            connected_target_commits = RevsetExpression::commits(commit_ids.clone())
611                .connected()
612                .evaluate(repo)
613                .map_err(|err| err.into_backend_error())?
614                .iter()
615                .commits(repo.store())
616                .try_collect()
617                .map_err(|err| err.into_backend_error())?;
618            connected_target_commits_internal_parents =
619                compute_internal_parents_within(&target_commit_ids, &connected_target_commits);
620
621            target_roots = connected_target_commits_internal_parents
622                .iter()
623                .filter(|&(commit_id, parents)| {
624                    target_commit_ids.contains(commit_id) && parents.is_empty()
625                })
626                .map(|(commit_id, _)| commit_id.clone())
627                .collect();
628        }
629        MoveCommitsTarget::Roots(root_ids) => {
630            if root_ids.is_empty() {
631                return Ok(ComputedMoveCommits::empty());
632            }
633
634            target_commit_ids = RevsetExpression::commits(root_ids.clone())
635                .descendants()
636                .evaluate(repo)
637                .map_err(|err| err.into_backend_error())?
638                .iter()
639                .try_collect()
640                .map_err(|err| err.into_backend_error())?;
641
642            connected_target_commits = target_commit_ids
643                .iter()
644                .map(|id| repo.store().get_commit(id))
645                .try_collect()?;
646            // We don't have to compute the internal parents for the connected target set,
647            // since the connected target set is the same as the target set.
648            connected_target_commits_internal_parents = HashMap::new();
649            target_roots = root_ids.iter().cloned().collect();
650        }
651    }
652
653    // If a commit outside the target set has a commit in the target set as a
654    // parent, then - after the transformation - it should have that commit's
655    // ancestors which are not in the target set as parents.
656    let mut target_commits_external_parents: HashMap<CommitId, IndexSet<CommitId>> = HashMap::new();
657    for id in target_commit_ids.iter().rev() {
658        let commit = repo.store().get_commit(id)?;
659        let mut new_parents = IndexSet::new();
660        for old_parent in commit.parent_ids() {
661            if let Some(parents) = target_commits_external_parents.get(old_parent) {
662                new_parents.extend(parents.iter().cloned());
663            } else {
664                new_parents.insert(old_parent.clone());
665            }
666        }
667        target_commits_external_parents.insert(commit.id().clone(), new_parents);
668    }
669
670    // If the new parents include a commit in the target set, replace it with the
671    // commit's ancestors which are outside the set.
672    // e.g. `jj rebase -r A --before A`
673    let new_parent_ids: Vec<_> = loc
674        .new_parent_ids
675        .iter()
676        .flat_map(|parent_id| {
677            if let Some(parent_ids) = target_commits_external_parents.get(parent_id) {
678                parent_ids.iter().cloned().collect_vec()
679            } else {
680                vec![parent_id.clone()]
681            }
682        })
683        .collect();
684
685    // If the new children include a commit in the target set, replace it with the
686    // commit's descendants which are outside the set.
687    // e.g. `jj rebase -r A --after A`
688    let new_children: Vec<_> = if loc
689        .new_child_ids
690        .iter()
691        .any(|id| target_commit_ids.contains(id))
692    {
693        let target_commits_descendants: Vec<_> =
694            RevsetExpression::commits(target_commit_ids.iter().cloned().collect_vec())
695                .union(
696                    &RevsetExpression::commits(target_commit_ids.iter().cloned().collect_vec())
697                        .children(),
698                )
699                .evaluate(repo)
700                .map_err(|err| err.into_backend_error())?
701                .iter()
702                .commits(repo.store())
703                .try_collect()
704                .map_err(|err| err.into_backend_error())?;
705
706        // For all commits in the target set, compute its transitive descendant commits
707        // which are outside of the target set by up to 1 generation.
708        let mut target_commit_external_descendants: HashMap<CommitId, IndexSet<Commit>> =
709            HashMap::new();
710        // Iterate through all descendants of the target set, going through children
711        // before parents.
712        for commit in &target_commits_descendants {
713            if !target_commit_external_descendants.contains_key(commit.id()) {
714                let children = if target_commit_ids.contains(commit.id()) {
715                    IndexSet::new()
716                } else {
717                    IndexSet::from([commit.clone()])
718                };
719                target_commit_external_descendants.insert(commit.id().clone(), children);
720            }
721
722            let children = target_commit_external_descendants
723                .get(commit.id())
724                .unwrap()
725                .iter()
726                .cloned()
727                .collect_vec();
728            for parent_id in commit.parent_ids() {
729                if target_commit_ids.contains(parent_id) {
730                    if let Some(target_children) =
731                        target_commit_external_descendants.get_mut(parent_id)
732                    {
733                        target_children.extend(children.iter().cloned());
734                    } else {
735                        target_commit_external_descendants
736                            .insert(parent_id.clone(), children.iter().cloned().collect());
737                    }
738                }
739            }
740        }
741
742        let mut new_children = Vec::new();
743        for id in &loc.new_child_ids {
744            if let Some(children) = target_commit_external_descendants.get(id) {
745                new_children.extend(children.iter().cloned());
746            } else {
747                new_children.push(repo.store().get_commit(id)?);
748            }
749        }
750        new_children
751    } else {
752        loc.new_child_ids
753            .iter()
754            .map(|id| repo.store().get_commit(id))
755            .try_collect()?
756    };
757
758    // Compute the parents of the new children, which will include the heads of the
759    // target set.
760    let new_children_parents: HashMap<_, _> = if !new_children.is_empty() {
761        // Compute the heads of the target set, which will be used as the parents of
762        // `new_children`.
763        let target_heads = compute_commits_heads(&target_commit_ids, &connected_target_commits);
764
765        new_children
766            .iter()
767            .map(|child_commit| {
768                let mut new_child_parent_ids = IndexSet::new();
769                for old_child_parent_id in child_commit.parent_ids() {
770                    // Replace target commits with their parents outside the target set.
771                    let old_child_parent_ids = if let Some(parents) =
772                        target_commits_external_parents.get(old_child_parent_id)
773                    {
774                        parents.iter().collect_vec()
775                    } else {
776                        vec![old_child_parent_id]
777                    };
778
779                    // If the original parents of the new children are the new parents of the
780                    // `target_heads`, replace them with the target heads since we are "inserting"
781                    // the target commits in between the new parents and the new children.
782                    for id in old_child_parent_ids {
783                        if new_parent_ids.contains(id) {
784                            new_child_parent_ids.extend(target_heads.clone());
785                        } else {
786                            new_child_parent_ids.insert(id.clone());
787                        }
788                    }
789                }
790
791                // If not already present, add `target_heads` as parents of the new child
792                // commit.
793                new_child_parent_ids.extend(target_heads.clone());
794
795                (
796                    child_commit.id().clone(),
797                    new_child_parent_ids.into_iter().collect_vec(),
798                )
799            })
800            .collect()
801    } else {
802        HashMap::new()
803    };
804
805    // Compute the set of commits to visit, which includes the target commits, the
806    // new children commits (if any), and their descendants.
807    let mut roots = target_roots.iter().cloned().collect_vec();
808    roots.extend(new_children.iter().ids().cloned());
809
810    let descendants = repo.find_descendants_for_rebase(roots.clone())?;
811    let commit_new_parents_map = descendants
812        .iter()
813        .map(|commit| -> BackendResult<_> {
814            let commit_id = commit.id();
815            let new_parent_ids =
816                if let Some(new_child_parents) = new_children_parents.get(commit_id) {
817                    // New child of the rebased target commits.
818                    new_child_parents.clone()
819                } else if target_commit_ids.contains(commit_id) {
820                    // Commit is in the target set.
821                    if target_roots.contains(commit_id) {
822                        // If the commit is a root of the target set, it should be rebased onto the
823                        // new destination.
824                        new_parent_ids.clone()
825                    } else {
826                        // Otherwise:
827                        // 1. Keep parents which are within the target set.
828                        // 2. Replace parents which are outside the target set but are part of the
829                        //    connected target set with their ancestor commits which are in the
830                        //    target set.
831                        // 3. Keep other parents outside the target set if they are not descendants
832                        //    of the new children of the target set.
833                        let mut new_parents = vec![];
834                        for parent_id in commit.parent_ids() {
835                            if target_commit_ids.contains(parent_id) {
836                                new_parents.push(parent_id.clone());
837                            } else if let Some(parents) =
838                                connected_target_commits_internal_parents.get(parent_id)
839                            {
840                                new_parents.extend(parents.iter().cloned());
841                            } else if !fallible_any(&new_children, |child| {
842                                repo.index().is_ancestor(child.id(), parent_id)
843                            })
844                            // TODO: indexing error shouldn't be a "BackendError"
845                            .map_err(|err| BackendError::Other(err.into()))?
846                            {
847                                new_parents.push(parent_id.clone());
848                            }
849                        }
850                        new_parents
851                    }
852                } else if commit
853                    .parent_ids()
854                    .iter()
855                    .any(|id| target_commits_external_parents.contains_key(id))
856                {
857                    // Commits outside the target set should have references to commits inside the
858                    // set replaced.
859                    let mut new_parents = vec![];
860                    for parent in commit.parent_ids() {
861                        if let Some(parents) = target_commits_external_parents.get(parent) {
862                            new_parents.extend(parents.iter().cloned());
863                        } else {
864                            new_parents.push(parent.clone());
865                        }
866                    }
867                    new_parents
868                } else {
869                    commit.parent_ids().iter().cloned().collect_vec()
870                };
871            Ok((commit.id().clone(), new_parent_ids))
872        })
873        .try_collect()?;
874
875    Ok(ComputedMoveCommits {
876        target_commit_ids,
877        descendants,
878        commit_new_parents_map,
879        to_abandon: HashSet::new(),
880    })
881}
882
883fn apply_move_commits(
884    mut_repo: &mut MutableRepo,
885    commits: ComputedMoveCommits,
886    options: &RebaseOptions,
887) -> BackendResult<MoveCommitsStats> {
888    let mut num_rebased_targets = 0;
889    let mut num_rebased_descendants = 0;
890    let mut num_skipped_rebases = 0;
891    let mut num_abandoned_empty = 0;
892
893    // Always keep empty commits when rebasing descendants.
894    let rebase_descendant_options = &RebaseOptions {
895        empty: EmptyBehavior::Keep,
896        rewrite_refs: options.rewrite_refs.clone(),
897        simplify_ancestor_merge: options.simplify_ancestor_merge,
898    };
899
900    let mut rebased_commits: HashMap<CommitId, RebasedCommit> = HashMap::new();
901    mut_repo.transform_commits(
902        commits.descendants,
903        &commits.commit_new_parents_map,
904        &options.rewrite_refs,
905        async |rewriter| {
906            let old_commit_id = rewriter.old_commit().id().clone();
907            if commits.to_abandon.contains(&old_commit_id) {
908                rewriter.abandon();
909            } else if rewriter.parents_changed() {
910                let is_target_commit = commits.target_commit_ids.contains(&old_commit_id);
911                let rebased_commit = rebase_commit_with_options(
912                    rewriter,
913                    if is_target_commit {
914                        options
915                    } else {
916                        rebase_descendant_options
917                    },
918                )?;
919                if let RebasedCommit::Abandoned { .. } = rebased_commit {
920                    num_abandoned_empty += 1;
921                } else if is_target_commit {
922                    num_rebased_targets += 1;
923                } else {
924                    num_rebased_descendants += 1;
925                }
926                rebased_commits.insert(old_commit_id, rebased_commit);
927            } else {
928                num_skipped_rebases += 1;
929            }
930
931            Ok(())
932        },
933    )?;
934
935    Ok(MoveCommitsStats {
936        num_rebased_targets,
937        num_rebased_descendants,
938        num_skipped_rebases,
939        num_abandoned_empty,
940        rebased_commits,
941    })
942}
943
944#[derive(Debug, Default)]
945pub struct DuplicateCommitsStats {
946    /// Map of original commit ID to newly duplicated commit.
947    pub duplicated_commits: IndexMap<CommitId, Commit>,
948    /// The number of descendant commits which were rebased onto the duplicated
949    /// commits.
950    pub num_rebased: u32,
951}
952
953/// Duplicates the given `target_commit_ids` onto a new location in the graph.
954///
955/// The roots of `target_commit_ids` are duplicated on top of the new
956/// `parent_commit_ids`, whilst other commits in `target_commit_ids` are
957/// duplicated on top of the newly duplicated commits in the target set. If
958/// `children_commit_ids` is not empty, the `children_commit_ids` will be
959/// rebased onto the heads of the duplicated target commits.
960///
961/// If `target_descriptions` is not empty, it will be consulted to retrieve the
962/// new descriptions of the target commits, falling back to the original if the
963/// map does not contain an entry for a given commit.
964///
965/// This assumes that commits in `children_commit_ids` can be rewritten. There
966/// should also be no cycles in the resulting graph, i.e. `children_commit_ids`
967/// should not be ancestors of `parent_commit_ids`. Commits in
968/// `target_commit_ids` should be in reverse topological order (children before
969/// parents).
970pub async fn duplicate_commits(
971    mut_repo: &mut MutableRepo,
972    target_commit_ids: &[CommitId],
973    target_descriptions: &HashMap<CommitId, String>,
974    parent_commit_ids: &[CommitId],
975    children_commit_ids: &[CommitId],
976) -> BackendResult<DuplicateCommitsStats> {
977    if target_commit_ids.is_empty() {
978        return Ok(DuplicateCommitsStats::default());
979    }
980
981    let mut duplicated_old_to_new: IndexMap<CommitId, Commit> = IndexMap::new();
982    let mut num_rebased = 0;
983
984    let target_commit_ids: IndexSet<_> = target_commit_ids.iter().cloned().collect();
985
986    let connected_target_commits: Vec<_> =
987        RevsetExpression::commits(target_commit_ids.iter().cloned().collect_vec())
988            .connected()
989            .evaluate(mut_repo)
990            .map_err(|err| err.into_backend_error())?
991            .iter()
992            .commits(mut_repo.store())
993            .try_collect()
994            .map_err(|err| err.into_backend_error())?;
995
996    // Commits in the target set should only have other commits in the set as
997    // parents, except the roots of the set, which persist their original
998    // parents.
999    // If a commit in the target set has a parent which is not in the set, but has
1000    // an ancestor which is in the set, then the commit will have that ancestor
1001    // as a parent instead.
1002    let target_commits_internal_parents = {
1003        let mut target_commits_internal_parents =
1004            compute_internal_parents_within(&target_commit_ids, &connected_target_commits);
1005        target_commits_internal_parents.retain(|id, _| target_commit_ids.contains(id));
1006        target_commits_internal_parents
1007    };
1008
1009    // Compute the roots of `target_commits`.
1010    let target_root_ids: HashSet<_> = target_commits_internal_parents
1011        .iter()
1012        .filter(|(_, parents)| parents.is_empty())
1013        .map(|(commit_id, _)| commit_id.clone())
1014        .collect();
1015
1016    // Compute the heads of the target set, which will be used as the parents of
1017    // the children commits.
1018    let target_head_ids = if !children_commit_ids.is_empty() {
1019        compute_commits_heads(&target_commit_ids, &connected_target_commits)
1020    } else {
1021        vec![]
1022    };
1023
1024    // Topological order ensures that any parents of the original commit are
1025    // either not in `target_commits` or were already duplicated.
1026    for original_commit_id in target_commit_ids.iter().rev() {
1027        let original_commit = mut_repo
1028            .store()
1029            .get_commit_async(original_commit_id)
1030            .await?;
1031        let new_parent_ids = if target_root_ids.contains(original_commit_id) {
1032            parent_commit_ids.to_vec()
1033        } else {
1034            target_commits_internal_parents
1035                .get(original_commit_id)
1036                .unwrap()
1037                .iter()
1038                // Replace parent IDs with their new IDs if they were duplicated.
1039                .map(|id| {
1040                    duplicated_old_to_new
1041                        .get(id)
1042                        .map_or(id, |commit| commit.id())
1043                        .clone()
1044                })
1045                .collect()
1046        };
1047        let mut new_commit_builder = CommitRewriter::new(mut_repo, original_commit, new_parent_ids)
1048            .rebase()
1049            .await?
1050            .clear_rewrite_source()
1051            .generate_new_change_id();
1052        if let Some(desc) = target_descriptions.get(original_commit_id) {
1053            new_commit_builder = new_commit_builder.set_description(desc);
1054        }
1055        duplicated_old_to_new.insert(original_commit_id.clone(), new_commit_builder.write()?);
1056    }
1057
1058    // Replace the original commit IDs in `target_head_ids` with the duplicated
1059    // commit IDs.
1060    let target_head_ids = target_head_ids
1061        .into_iter()
1062        .map(|commit_id| {
1063            duplicated_old_to_new
1064                .get(&commit_id)
1065                .map_or(commit_id, |commit| commit.id().clone())
1066        })
1067        .collect_vec();
1068
1069    // Rebase new children onto the target heads.
1070    let children_commit_ids_set: HashSet<CommitId> = children_commit_ids.iter().cloned().collect();
1071    mut_repo.transform_descendants(children_commit_ids.to_vec(), async |mut rewriter| {
1072        if children_commit_ids_set.contains(rewriter.old_commit().id()) {
1073            let mut child_new_parent_ids = IndexSet::new();
1074            for old_parent_id in rewriter.old_commit().parent_ids() {
1075                // If the original parents of the new children are the new parents of
1076                // `target_head_ids`, replace them with `target_head_ids` since we are
1077                // "inserting" the target commits in between the new parents and the new
1078                // children.
1079                if parent_commit_ids.contains(old_parent_id) {
1080                    child_new_parent_ids.extend(target_head_ids.clone());
1081                } else {
1082                    child_new_parent_ids.insert(old_parent_id.clone());
1083                }
1084            }
1085            // If not already present, add `target_head_ids` as parents of the new child
1086            // commit.
1087            child_new_parent_ids.extend(target_head_ids.clone());
1088            rewriter.set_new_parents(child_new_parent_ids.into_iter().collect());
1089        }
1090        num_rebased += 1;
1091        rewriter.rebase().await?.write()?;
1092        Ok(())
1093    })?;
1094
1095    Ok(DuplicateCommitsStats {
1096        duplicated_commits: duplicated_old_to_new,
1097        num_rebased,
1098    })
1099}
1100
1101/// Duplicates the given `target_commits` onto their original parents or other
1102/// duplicated commits.
1103///
1104/// Commits in `target_commits` should be in reverse topological order (children
1105/// before parents).
1106///
1107/// If `target_descriptions` is not empty, it will be consulted to retrieve the
1108/// new descriptions of the target commits, falling back to the original if
1109/// the map does not contain an entry for a given commit.
1110pub fn duplicate_commits_onto_parents(
1111    mut_repo: &mut MutableRepo,
1112    target_commits: &[CommitId],
1113    target_descriptions: &HashMap<CommitId, String>,
1114) -> BackendResult<DuplicateCommitsStats> {
1115    if target_commits.is_empty() {
1116        return Ok(DuplicateCommitsStats::default());
1117    }
1118
1119    let mut duplicated_old_to_new: IndexMap<CommitId, Commit> = IndexMap::new();
1120
1121    // Topological order ensures that any parents of the original commit are
1122    // either not in `target_commits` or were already duplicated.
1123    for original_commit_id in target_commits.iter().rev() {
1124        let original_commit = mut_repo.store().get_commit(original_commit_id)?;
1125        let new_parent_ids = original_commit
1126            .parent_ids()
1127            .iter()
1128            .map(|id| {
1129                duplicated_old_to_new
1130                    .get(id)
1131                    .map_or(id, |commit| commit.id())
1132                    .clone()
1133            })
1134            .collect();
1135        let mut new_commit_builder = mut_repo
1136            .rewrite_commit(&original_commit)
1137            .clear_rewrite_source()
1138            .generate_new_change_id()
1139            .set_parents(new_parent_ids);
1140        if let Some(desc) = target_descriptions.get(original_commit_id) {
1141            new_commit_builder = new_commit_builder.set_description(desc);
1142        }
1143        duplicated_old_to_new.insert(original_commit_id.clone(), new_commit_builder.write()?);
1144    }
1145
1146    Ok(DuplicateCommitsStats {
1147        duplicated_commits: duplicated_old_to_new,
1148        num_rebased: 0,
1149    })
1150}
1151
1152/// Computes the internal parents of all commits in a connected commit graph,
1153/// allowing only commits in the target set as parents.
1154///
1155/// The parents of each commit are identical to the ones found using a preorder
1156/// DFS of the node's ancestors, starting from the node itself, and avoiding
1157/// traversing an edge if the parent is in the target set. `graph_commits`
1158/// should be in reverse topological order.
1159fn compute_internal_parents_within(
1160    target_commit_ids: &IndexSet<CommitId>,
1161    graph_commits: &[Commit],
1162) -> HashMap<CommitId, IndexSet<CommitId>> {
1163    let mut internal_parents: HashMap<CommitId, IndexSet<CommitId>> = HashMap::new();
1164    for commit in graph_commits.iter().rev() {
1165        // The roots of the set will not have any parents found in `internal_parents`,
1166        // and will be stored as an empty vector.
1167        let mut new_parents = IndexSet::new();
1168        for old_parent in commit.parent_ids() {
1169            if target_commit_ids.contains(old_parent) {
1170                new_parents.insert(old_parent.clone());
1171            } else if let Some(parents) = internal_parents.get(old_parent) {
1172                new_parents.extend(parents.iter().cloned());
1173            }
1174        }
1175        internal_parents.insert(commit.id().clone(), new_parents);
1176    }
1177    internal_parents
1178}
1179
1180/// Computes the heads of commits in the target set, given the list of
1181/// `target_commit_ids` and a connected graph of commits.
1182///
1183/// `connected_target_commits` should be in reverse topological order (children
1184/// before parents).
1185fn compute_commits_heads(
1186    target_commit_ids: &IndexSet<CommitId>,
1187    connected_target_commits: &[Commit],
1188) -> Vec<CommitId> {
1189    let mut target_head_ids: HashSet<CommitId> = HashSet::new();
1190    for commit in connected_target_commits.iter().rev() {
1191        target_head_ids.insert(commit.id().clone());
1192        for old_parent in commit.parent_ids() {
1193            target_head_ids.remove(old_parent);
1194        }
1195    }
1196    connected_target_commits
1197        .iter()
1198        .rev()
1199        .filter(|commit| {
1200            target_head_ids.contains(commit.id()) && target_commit_ids.contains(commit.id())
1201        })
1202        .map(|commit| commit.id().clone())
1203        .collect_vec()
1204}
1205
1206#[derive(Debug)]
1207pub struct CommitWithSelection {
1208    pub commit: Commit,
1209    pub selected_tree: MergedTree,
1210    pub parent_tree: MergedTree,
1211}
1212
1213impl CommitWithSelection {
1214    /// Returns true if the selection contains all changes in the commit.
1215    pub fn is_full_selection(&self) -> bool {
1216        self.selected_tree.tree_ids() == self.commit.tree_ids()
1217    }
1218
1219    /// Returns true if the selection matches the parent tree (contains no
1220    /// changes from the commit).
1221    ///
1222    /// Both `is_full_selection()` and `is_empty_selection()`
1223    /// can be true if the commit is itself empty.
1224    pub fn is_empty_selection(&self) -> bool {
1225        self.selected_tree.tree_ids() == self.parent_tree.tree_ids()
1226    }
1227
1228    /// Returns a diff of labeled trees which represents the selected changes.
1229    /// This can be used with `MergedTree::merge` and `Merge::from_diffs` to
1230    /// apply the selected changes to a tree.
1231    pub fn diff_with_labels(
1232        &self,
1233        parent_tree_label: &str,
1234        selected_tree_label: &str,
1235        full_selection_label: &str,
1236    ) -> BackendResult<Diff<(MergedTree, String)>> {
1237        let parent_tree_label = format!(
1238            "{} ({parent_tree_label})",
1239            self.commit.parents_conflict_label()?
1240        );
1241
1242        let commit_label = self.commit.conflict_label();
1243        let selected_tree_label = if self.is_full_selection() {
1244            format!("{commit_label} ({full_selection_label})")
1245        } else {
1246            format!("{selected_tree_label} (from {commit_label})")
1247        };
1248
1249        Ok(Diff::new(
1250            (self.parent_tree.clone(), parent_tree_label),
1251            (self.selected_tree.clone(), selected_tree_label),
1252        ))
1253    }
1254}
1255
1256/// Resulting commit builder and stats to be returned by [`squash_commits()`].
1257#[must_use]
1258pub struct SquashedCommit<'repo> {
1259    /// New destination commit will be created by this builder.
1260    pub commit_builder: CommitBuilder<'repo>,
1261    /// List of abandoned source commits.
1262    pub abandoned_commits: Vec<Commit>,
1263}
1264
1265/// Squash `sources` into `destination` and return a [`SquashedCommit`] for the
1266/// resulting commit. Caller is responsible for setting the description and
1267/// finishing the commit.
1268pub fn squash_commits<'repo>(
1269    repo: &'repo mut MutableRepo,
1270    sources: &[CommitWithSelection],
1271    destination: &Commit,
1272    keep_emptied: bool,
1273) -> BackendResult<Option<SquashedCommit<'repo>>> {
1274    struct SourceCommit<'a> {
1275        commit: &'a CommitWithSelection,
1276        diff: Diff<(MergedTree, String)>,
1277        abandon: bool,
1278    }
1279    let mut source_commits = vec![];
1280    for source in sources {
1281        let abandon = !keep_emptied && source.is_full_selection();
1282        if !abandon && source.is_empty_selection() {
1283            // Nothing selected from this commit. If it's abandoned (i.e. already empty), we
1284            // still include it so `jj squash` can be used for abandoning an empty commit in
1285            // the middle of a stack.
1286            continue;
1287        }
1288
1289        // TODO: Do we want to optimize the case of moving to the parent commit (`jj
1290        // squash -r`)? The source tree will be unchanged in that case.
1291        source_commits.push(SourceCommit {
1292            commit: source,
1293            diff: source.diff_with_labels(
1294                "parents of squashed revision",
1295                "selected changes for squash",
1296                "squashed revision",
1297            )?,
1298            abandon,
1299        });
1300    }
1301
1302    if source_commits.is_empty() {
1303        return Ok(None);
1304    }
1305
1306    let mut abandoned_commits = vec![];
1307    for source in &source_commits {
1308        if source.abandon {
1309            repo.record_abandoned_commit(&source.commit.commit);
1310            abandoned_commits.push(source.commit.commit.clone());
1311        } else {
1312            let source_tree = source.commit.commit.tree();
1313            // Apply the reverse of the selected changes onto the source
1314            let new_source_tree = MergedTree::merge(Merge::from_diffs(
1315                (source_tree, source.commit.commit.conflict_label()),
1316                [source.diff.clone().invert()],
1317            ))
1318            .block_on()?;
1319            repo.rewrite_commit(&source.commit.commit)
1320                .set_tree(new_source_tree)
1321                .write()?;
1322        }
1323    }
1324
1325    let mut rewritten_destination = destination.clone();
1326    if fallible_any(sources, |source| {
1327        repo.index()
1328            .is_ancestor(source.commit.id(), destination.id())
1329    })
1330    // TODO: indexing error shouldn't be a "BackendError"
1331    .map_err(|err| BackendError::Other(err.into()))?
1332    {
1333        // If we're moving changes to a descendant, first rebase descendants onto the
1334        // rewritten sources. Otherwise it will likely already have the content
1335        // changes we're moving, so applying them will have no effect and the
1336        // changes will disappear.
1337        let options = RebaseOptions::default();
1338        repo.rebase_descendants_with_options(&options, |old_commit, rebased_commit| {
1339            if old_commit.id() != destination.id() {
1340                return;
1341            }
1342            rewritten_destination = match rebased_commit {
1343                RebasedCommit::Rewritten(commit) => commit,
1344                RebasedCommit::Abandoned { .. } => panic!("all commits should be kept"),
1345            };
1346        })?;
1347    }
1348    let mut predecessors = vec![destination.id().clone()];
1349    predecessors.extend(
1350        source_commits
1351            .iter()
1352            .map(|source| source.commit.commit.id().clone()),
1353    );
1354    // Apply the selected changes onto the destination
1355    let destination_tree = MergedTree::merge(Merge::from_diffs(
1356        (
1357            rewritten_destination.tree(),
1358            format!("{} (squash destination)", destination.conflict_label()),
1359        ),
1360        source_commits.into_iter().map(|source| source.diff),
1361    ))
1362    .block_on()?;
1363
1364    let commit_builder = repo
1365        .rewrite_commit(&rewritten_destination)
1366        .set_tree(destination_tree)
1367        .set_predecessors(predecessors);
1368    Ok(Some(SquashedCommit {
1369        commit_builder,
1370        abandoned_commits,
1371    }))
1372}
1373
1374/// Find divergent commits from the target that are already present with
1375/// identical contents in the destination. These commits should be able to be
1376/// safely abandoned.
1377pub fn find_duplicate_divergent_commits(
1378    repo: &dyn Repo,
1379    new_parent_ids: &[CommitId],
1380    target: &MoveCommitsTarget,
1381) -> BackendResult<Vec<Commit>> {
1382    let target_commits: Vec<Commit> = match target {
1383        MoveCommitsTarget::Commits(commit_ids) => commit_ids
1384            .iter()
1385            .map(|commit_id| repo.store().get_commit(commit_id))
1386            .try_collect()?,
1387        MoveCommitsTarget::Roots(root_ids) => RevsetExpression::commits(root_ids.clone())
1388            .descendants()
1389            .evaluate(repo)
1390            .map_err(|err| err.into_backend_error())?
1391            .iter()
1392            .commits(repo.store())
1393            .try_collect()
1394            .map_err(|err| err.into_backend_error())?,
1395    };
1396    let target_commit_ids: HashSet<&CommitId> = target_commits.iter().map(Commit::id).collect();
1397
1398    // For each divergent change being rebased, we want to find all of the other
1399    // commits with the same change ID which are not being rebased.
1400    let divergent_changes: Vec<(&Commit, Vec<CommitId>)> = target_commits
1401        .iter()
1402        .map(|target_commit| -> Result<_, BackendError> {
1403            let mut ancestor_candidates = repo
1404                .resolve_change_id(target_commit.change_id())
1405                // TODO: indexing error shouldn't be a "BackendError"
1406                .map_err(|err| BackendError::Other(err.into()))?
1407                .and_then(ResolvedChangeTargets::into_visible)
1408                .unwrap_or_default();
1409            ancestor_candidates.retain(|commit_id| !target_commit_ids.contains(commit_id));
1410            Ok((target_commit, ancestor_candidates))
1411        })
1412        .filter_ok(|(_, candidates)| !candidates.is_empty())
1413        .try_collect()?;
1414    if divergent_changes.is_empty() {
1415        return Ok(Vec::new());
1416    }
1417
1418    let target_root_ids = match target {
1419        MoveCommitsTarget::Commits(commit_ids) => commit_ids,
1420        MoveCommitsTarget::Roots(root_ids) => root_ids,
1421    };
1422
1423    // We only care about divergent changes which are new ancestors of the rebased
1424    // commits, not ones which were already ancestors of the rebased commits.
1425    let is_new_ancestor = RevsetExpression::commits(target_root_ids.clone())
1426        .range(&RevsetExpression::commits(new_parent_ids.to_owned()))
1427        .evaluate(repo)
1428        .map_err(|err| err.into_backend_error())?
1429        .containing_fn();
1430
1431    let mut duplicate_divergent = Vec::new();
1432    // Checking every pair of commits between these two sets could be expensive if
1433    // there are several commits with the same change ID. However, it should be
1434    // uncommon to have more than a couple commits with the same change ID being
1435    // rebased at the same time, so it should be good enough in practice.
1436    for (target_commit, ancestor_candidates) in divergent_changes {
1437        for ancestor_candidate_id in ancestor_candidates {
1438            if !is_new_ancestor(&ancestor_candidate_id).map_err(|err| err.into_backend_error())? {
1439                continue;
1440            }
1441
1442            let ancestor_candidate = repo.store().get_commit(&ancestor_candidate_id)?;
1443            let new_tree =
1444                rebase_to_dest_parent(repo, slice::from_ref(target_commit), &ancestor_candidate)?;
1445            // Check whether the rebased commit would have the same tree as the existing
1446            // commit if they had the same parents. If so, we can skip this rebased commit.
1447            if new_tree.tree_ids() == ancestor_candidate.tree_ids() {
1448                duplicate_divergent.push(target_commit.clone());
1449                break;
1450            }
1451        }
1452    }
1453    Ok(duplicate_divergent)
1454}