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