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