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