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