Skip to main content

jj_lib/
converge.rs

1// Copyright 2026 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//! Utility for solving divergence. See
16//! <https://github.com/jj-vcs/jj/blob/main/docs/design/jj-converge-command.md>
17//! for more details.
18
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::hash::Hash;
22use std::ops::Deref as _;
23use std::rc::Rc;
24use std::sync::Arc;
25use std::sync::Mutex;
26
27use futures::StreamExt as _;
28use futures::TryStreamExt as _;
29use futures::executor::block_on_stream;
30use futures::future::try_join_all;
31use itertools::Itertools as _;
32use jj_lib::backend::BackendError;
33use jj_lib::backend::BackendResult;
34use jj_lib::backend::ChangeId;
35use jj_lib::backend::CommitId;
36use jj_lib::backend::Signature;
37use jj_lib::backend::TreeId;
38use jj_lib::commit::Commit;
39use jj_lib::conflict_labels::ConflictLabels;
40use jj_lib::evolution::WalkPredecessorsError;
41use jj_lib::evolution::walk_predecessors;
42use jj_lib::graph_dominators::FlowGraph;
43use jj_lib::graph_dominators::SimpleDirectedGraph;
44use jj_lib::graph_dominators::ValueCache;
45use jj_lib::index::IndexError;
46use jj_lib::merge::Merge;
47use jj_lib::merge::MergeBuilder;
48use jj_lib::merge::SameChange;
49use jj_lib::merged_tree::MergedTree;
50use jj_lib::repo::MutableRepo;
51use jj_lib::repo::ReadonlyRepo;
52use jj_lib::repo::Repo as _;
53use jj_lib::revset::ResolvedRevsetExpression;
54use jj_lib::revset::RevsetEvaluationError;
55use jj_lib::revset::RevsetExpression;
56use jj_lib::rewrite::merge_commit_trees_no_resolve;
57use jj_lib::store::Store;
58use thiserror::Error;
59
60/// Maps change-ids to commits with that change-id.
61pub type CommitsByChangeId = HashMap<ChangeId, HashMap<CommitId, Commit>>;
62
63/// The result of attempting to converge a particular attribute (description,
64/// author, parents, tree) of a set of divergent commits.
65#[derive(Debug, PartialEq, Eq, Clone)]
66pub enum ConvergedAttribute<T> {
67    /// The attribute was successfully merged.
68    Solved(T),
69    /// The attribute could not be merged automatically.
70    Unsolved {
71        /// This is a hint to the caller: merge the attribute of the divergent
72        /// commits (minus the excluded_divergent_commits), using this
73        /// commit as the base.
74        base_commit: CommitId,
75        /// This is a hint to the caller: these divergent commits should be
76        /// excluded from the merge.
77        excluded_divergent_commits: HashSet<CommitId>,
78    },
79}
80
81/// The proposed solution for converging a change.
82#[derive(Debug, PartialEq, Eq, Clone)]
83pub struct ConvergeResult {
84    /// The proposed author.
85    pub author: ConvergedAttribute<Signature>,
86    /// The proposed description.
87    pub description: ConvergedAttribute<String>,
88    /// The proposed parents.
89    pub parents: ConvergedAttribute<Vec<CommitId>>,
90    /// The proposed tree. Not set if we haven't converged the parents yet.
91    pub tree: Option<TreeIdsAndLabels>,
92}
93
94/// Errors that can occur during converge.
95#[derive(Debug, Error)]
96pub enum ConvergeError {
97    /// A backend error occurred.
98    #[error(transparent)]
99    Backend(#[from] BackendError),
100    /// An index error occurred.
101    #[error(transparent)]
102    Index(#[from] IndexError),
103    /// An error occurred while evaluating the revset expression for finding
104    /// divergent commits.
105    #[error(transparent)]
106    RevsetEvaluation(#[from] RevsetEvaluationError),
107    /// An error occurred while traversing the evolution graph of the divergent
108    /// commits.
109    #[error(transparent)]
110    WalkPredecessors(#[from] WalkPredecessorsError),
111    /// An IO error occurred.
112    #[error(transparent)]
113    IO(#[from] std::io::Error),
114    /// An unexpected error occurred.
115    #[error(transparent)]
116    Other(Box<dyn std::error::Error + Send + Sync>),
117}
118
119/// Evaluates the revset expression and returns those commits that are
120/// divergent, in the sense that the expression matches two or more commits in
121/// the result with the same change-id.
122///
123/// The commits are keyed by their change-id.
124pub async fn find_divergent_changes(
125    repo: &Arc<ReadonlyRepo>,
126    revset_expression: Arc<ResolvedRevsetExpression>,
127) -> Result<CommitsByChangeId, RevsetEvaluationError> {
128    let mut result = CommitsByChangeId::new();
129    let mut stream = revset_expression.evaluate(repo.as_ref())?.stream();
130    while let Some(commit_id) = stream.try_next().await? {
131        let commit = repo.store().get_commit_async(&commit_id).await?;
132        result
133            .entry(commit.change_id().clone())
134            .or_default()
135            .insert(commit.id().clone(), commit);
136    }
137    // Remove entries that have only a single commit — we only care about
138    // changes with multiple divergent commits.
139    result.retain(|_, commits| commits.len() > 1);
140    Ok(result)
141}
142
143/// Attempts to solve divergence in the divergent commits given by the
144/// TruncatedEvolutionGraph. The caller can provide any subset of author,
145/// description, parents, and tree, to be used in the solution commit. An
146/// attempt is made to automatically produce a value for those attributes not
147/// given by the user.
148pub async fn converge_change(
149    truncated_evolution_graph: &TruncatedEvolutionGraph,
150    author: Option<Signature>,
151    description: Option<String>,
152    parents: Option<Vec<CommitId>>,
153    tree: Option<TreeIdsAndLabels>,
154) -> Result<ConvergeResult, ConvergeError> {
155    let author = if let Some(author) = author {
156        ConvergedAttribute::Solved(author)
157    } else {
158        converge_author(truncated_evolution_graph).await?
159    };
160    let description = if let Some(description) = description {
161        ConvergedAttribute::Solved(description)
162    } else {
163        converge_description(truncated_evolution_graph).await?
164    };
165    let parents = if let Some(parents) = parents {
166        ConvergedAttribute::Solved(parents)
167    } else {
168        converge_parents(truncated_evolution_graph).await?
169    };
170
171    let tree = if let Some(tree) = tree {
172        Some(tree)
173    } else if let ConvergedAttribute::Solved(parents) = &parents {
174        let tree = converge_trees(truncated_evolution_graph, parents).await?;
175        Some(TreeIdsAndLabels::new(tree))
176    } else {
177        None
178    };
179
180    Ok(ConvergeResult {
181        author,
182        description,
183        parents,
184        tree,
185    })
186}
187
188/// Adds a new commit for the proposed solution, as a successor of the divergent
189/// commits.
190pub async fn apply_solution(
191    author: Signature,
192    description: String,
193    parents: Vec<CommitId>,
194    tree: TreeIdsAndLabels,
195    change_id: ChangeId,
196    divergent_commit_ids: &Vec<CommitId>,
197    repo_mut: &mut MutableRepo,
198) -> Result<(Commit, usize), ConvergeError> {
199    let merged_tree = tree.to_merged_tree(repo_mut.store());
200    let solution = repo_mut
201        .new_commit(parents, merged_tree)
202        .set_change_id(change_id.clone())
203        .set_description(description)
204        .set_author(author)
205        .set_predecessors(divergent_commit_ids.clone())
206        .write()
207        .await?;
208    for divergent_commit_id in divergent_commit_ids {
209        repo_mut.set_rewritten_commit(divergent_commit_id.clone(), solution.id().clone());
210    }
211    let num_rebased = repo_mut.rebase_descendants().await?;
212    Ok((solution, num_rebased))
213}
214
215/// The truncated evolution graph for a divergent change.
216///
217/// This is similar to the evolog graph, but truncated in the sense that it only
218/// contains commits that are for the given change-id, and only goes as far as
219/// the closest common dominator of the divergent commits.
220pub struct TruncatedEvolutionGraph {
221    // The repo.
222    repo: Arc<ReadonlyRepo>,
223    // The commits to converge.
224    divergent_commits: Vec<Commit>,
225    // The ids of the commits to converge.
226    divergent_commit_ids: Vec<CommitId>,
227    /// The evolution graph of the divergent commits, with edges X->Y if commit
228    /// X is a predecessor of commit Y and both X and Y have the same
229    /// divergent change-id. The graph is not necessarily a tree (commits
230    /// may have multiple predecessors). The start node is the evolution
231    /// fork point.
232    pub flow_graph: FlowGraph<CommitId>,
233}
234
235impl TruncatedEvolutionGraph {
236    /// Builds a truncated evolution graph for the given divergent commits,
237    /// which are expected to all have the same change-id.
238    pub async fn new(
239        repo: Arc<ReadonlyRepo>,
240        divergent_commits: Vec<Commit>,
241    ) -> Result<Self, ConvergeError> {
242        validate(
243            divergent_commits.len() > 1,
244            &format!(
245                "Expected multiple divergent commits, got {}",
246                divergent_commits.len()
247            ),
248        )?;
249
250        let divergent_commit_ids = divergent_commits
251            .iter()
252            .map(|c| c.id().clone())
253            .collect_vec();
254
255        // Ensure all provided divergent commits belong to the same change-id.
256        // Note: divergent_commits is not empty, so it is ok to unwrap.
257        let divergent_change_id = if divergent_commits.iter().map(|c| c.change_id()).all_equal() {
258            divergent_commits.first().unwrap().change_id().clone()
259        } else {
260            return Err(ConvergeError::Other(
261                "all divergent commits must have the same change-id".into(),
262            ));
263        };
264
265        // The list of edges, with commits pointing to their successors.
266        let mut edges = vec![];
267        let mut seen = HashSet::new();
268        let mut to_visit = HashSet::with_capacity(divergent_commit_ids.len());
269        to_visit.extend(divergent_commit_ids.iter().cloned());
270
271        let evolution_nodes = block_on_stream(
272            walk_predecessors(&repo, divergent_commit_ids.as_slice()).boxed_local(),
273        );
274
275        // These are the commits in the graph that have no predecessors. Typically
276        // there is exactly one entry in initial_nodes (the first commit for the
277        // change-id).
278        let mut initial_nodes = vec![];
279
280        for node in evolution_nodes {
281            let entry = node?;
282            let commit_id = entry.commit.id();
283            if *entry.commit.change_id() != divergent_change_id {
284                // Skip commits with unrelated change ids.
285                continue;
286            }
287            to_visit.remove(commit_id);
288            if !seen.insert(commit_id.clone()) {
289                // TODO: think about this some more. Can 2 different operations result in the
290                // same commit? Maybe the key should be (commit-id, operation-id).
291
292                // Note: currently walk_predecessors returns an error if the graph is cyclic, so
293                // we shouldn't encounter the same commit twice. But in the future we could
294                // allow cyclic evolution, and if we do there is no reason to disallow it here.
295                // By continuing we future proof this.
296                continue;
297            }
298            let predecessors = entry
299                .predecessors()
300                .await?
301                .iter()
302                .filter_map(|commit| {
303                    if *commit.change_id() == divergent_change_id {
304                        Some(commit.id().clone())
305                    } else {
306                        None
307                    }
308                })
309                .collect_vec();
310            for predecessor in &predecessors {
311                edges.push((predecessor.clone(), commit_id.clone()));
312            }
313            if predecessors.is_empty() {
314                initial_nodes.push(commit_id.clone());
315                if to_visit.is_empty() {
316                    break;
317                }
318            } else {
319                to_visit.extend(predecessors);
320            }
321        }
322
323        validate(
324            !initial_nodes.is_empty(),
325            "Unexpected error: initial_nodes should not be empty",
326        )?;
327
328        // By definition the flow graph must have a single initial node.
329        let initial_node = if initial_nodes.len() == 1 {
330            initial_nodes[0].clone()
331        } else {
332            // In graphs with multiple "real" initial nodes we introduce a virtual initial
333            // node (the root commit) and pretend the two or more "real" initial nodes are
334            // successors of the root commit.
335            let root_commit_id = repo.store().root_commit_id().clone();
336            for initial_node in initial_nodes {
337                edges.push((root_commit_id.clone(), initial_node));
338            }
339            root_commit_id
340        };
341
342        let flow_graph = FlowGraph::new(SimpleDirectedGraph::new(edges), initial_node);
343        Ok(Self {
344            repo,
345            divergent_commits,
346            divergent_commit_ids,
347            flow_graph,
348        })
349    }
350
351    /// Returns the repo.
352    pub fn repo(&self) -> &Arc<ReadonlyRepo> {
353        &self.repo
354    }
355
356    /// Returns the divergent commits.
357    pub fn divergent_commits(&self) -> &Vec<Commit> {
358        &self.divergent_commits
359    }
360
361    /// Returns the commit ids of the divergent commits.
362    pub fn divergent_commit_ids(&self) -> &Vec<CommitId> {
363        &self.divergent_commit_ids
364    }
365
366    /// Returns the change-id of the divergent commits. All divergent commits
367    /// are expected to have the same change-id.
368    pub fn change_id(&self) -> &ChangeId {
369        self.divergent_commits[0].change_id()
370    }
371}
372
373async fn converge_author(
374    graph: &TruncatedEvolutionGraph,
375) -> Result<ConvergedAttribute<Signature>, ConvergeError> {
376    let value_fn = async |c: &Commit| Ok(c.author().clone());
377    let excluded_divergent_commits = HashSet::default();
378    let (value_merge, base_commit) =
379        create_value_merge(graph, &excluded_divergent_commits, value_fn).await?;
380    if let Some(value) = value_merge.resolve_trivial(SameChange::Accept) {
381        Ok(ConvergedAttribute::Solved(value.clone()))
382    } else {
383        Ok(ConvergedAttribute::Unsolved {
384            base_commit,
385            excluded_divergent_commits: HashSet::default(),
386        })
387    }
388}
389
390async fn converge_description(
391    graph: &TruncatedEvolutionGraph,
392) -> Result<ConvergedAttribute<String>, ConvergeError> {
393    let value_fn = async |c: &Commit| Ok(c.description().to_string());
394    let excluded_divergent_commits = HashSet::default();
395    let (value_merge, base_commit) =
396        create_value_merge(graph, &excluded_divergent_commits, value_fn).await?;
397    if let Some(value) = value_merge.resolve_trivial(SameChange::Accept) {
398        Ok(ConvergedAttribute::Solved(value.clone()))
399    } else {
400        Ok(ConvergedAttribute::Unsolved {
401            base_commit,
402            excluded_divergent_commits: HashSet::default(),
403        })
404    }
405}
406
407async fn converge_parents(
408    graph: &TruncatedEvolutionGraph,
409) -> Result<ConvergedAttribute<Vec<CommitId>>, ConvergeError> {
410    // Filter out divergent commits that are descendants of other divergent commits
411    // (we cannot use the parents of those commits because that would introduce
412    // cycles when we rebase everything on top of the parents).
413    let viable_commits = remove_descendants(graph.repo(), graph.divergent_commit_ids()).await?;
414    let excluded_divergent_commits: HashSet<CommitId> = graph
415        .divergent_commit_ids()
416        .iter()
417        .filter(|commit_id| !viable_commits.contains(commit_id))
418        .cloned()
419        .collect();
420
421    let get_parents_fn = async |c: &Commit| Ok(c.parent_ids().to_vec());
422    let (value_merge, base_commit) =
423        create_value_merge(graph, &excluded_divergent_commits, get_parents_fn).await?;
424    if let Some(value) = value_merge.resolve_trivial(SameChange::Accept) {
425        Ok(ConvergedAttribute::Solved(value.clone()))
426    } else {
427        Ok(ConvergedAttribute::Unsolved {
428            base_commit,
429            excluded_divergent_commits,
430        })
431    }
432}
433
434/// A MergedTree, without the `Arc<Store>`. That allows us to derive Eq and Hash
435/// for it, which we need in some algorithms.
436#[derive(Eq, Hash, PartialEq, Clone, Debug)]
437pub struct TreeIdsAndLabels {
438    /// The tree IDs of the merged tree.
439    pub tree_ids: Merge<TreeId>,
440    /// Conflict labels of the merged tree.
441    pub labels: ConflictLabels,
442}
443
444impl TreeIdsAndLabels {
445    /// Creates a new TreeIdsAndLabels.
446    pub fn new(merged_tree: MergedTree) -> Self {
447        let (tree_ids, labels) = merged_tree.into_tree_ids_and_labels();
448        Self { tree_ids, labels }
449    }
450
451    /// Converts the TreeIdsAndLabels into a MergedTree.
452    pub fn to_merged_tree(&self, store: &Arc<Store>) -> MergedTree {
453        MergedTree::new(store.clone(), self.tree_ids.clone(), self.labels.clone())
454    }
455}
456
457// Assume A, B, C are the divergent commits, P is the solution parents (i.e. the
458// parents chosen by converge_parents), and F is a commit chosen as a "good base
459// for converging trees" as explained below.
460//
461// Notation:
462// * MCTNR: merge_commit_trees_no_resolve
463// * F^: MCTNR(F.parents()), i.e. the unresolved MergedTree of the parents of F.
464// * F': the resolved MergedTree of F rebased on top of the tree of P
465// * A': the resolved MergedTree of A rebased on top of the tree of P
466// * B': the resolved MergedTree of B rebased on top of the tree of P
467// * C': the resolved MergedTree of C rebased on top of the tree of P
468//
469// Let X be an arbitrary commit. X' is given by:
470// X' = MergedTree::merge{ MCTNR(P) + (X.tree - X^) } =
471//    = MergedTree::merge{ MCTNR(P) + (X.tree - MCTNR(X.parents())) }
472//
473// converge_trees returns:
474// Solution = MergedTree::merge{ F' + (A' - F') + (B' - F') + (C' - F') }
475//
476// What is F? What is a "good base for converging trees"? F is calculated as
477// follows:
478// 1. For each commit X in the truncated evolution graph, we calculate
479//    X'.tree_ids()
480// 2. We build the "Value Transition Graph" of the values from step 1, with
481//    edges between values corresponding to edges in the truncated evolution
482//    graph: if commit X is a predecessor of commit Y, then the value transition
483//    graph has an edge from X'.tree_ids() to Y'.tree_ids()
484// 3. We find the dominator value of this Value Transition Graph
485// 4. The dominator value is "produced" from one or more commits in the
486//    truncated evolution graph
487// 5. F is any of those producer commits (we pick the first one)
488async fn converge_trees(
489    truncated_evolution_graph: &TruncatedEvolutionGraph,
490    parents: &[CommitId],
491) -> Result<MergedTree, ConvergeError> {
492    let repo = truncated_evolution_graph.repo();
493    let parent_commits: Vec<Commit> =
494        try_join_all(parents.iter().map(|id| repo.store().get_commit_async(id))).await?;
495    let parents_merged_tree = merge_commit_trees_no_resolve(repo.as_ref(), &parent_commits).await?;
496    let rebased_resolved_trees = Arc::new(Mutex::new(HashMap::<CommitId, TreeIdsAndLabels>::new()));
497
498    // We first compute the dominator value of the trees (in the value history graph
499    // of the trees), together with the commit(s) that produce that tree. Any
500    // such commit is a good candidate to be used as the base of the merge.
501
502    let value_fn = async |commit: &Commit| -> Result<Merge<TreeId>, ConvergeError> {
503        let tree_ids_and_labels = TreeIdsAndLabels::new(
504            rebase_tree_onto_solution_parents(commit, parents, &parents_merged_tree, repo).await?,
505        );
506        rebased_resolved_trees
507            .lock()
508            .unwrap()
509            .insert(commit.id().clone(), tree_ids_and_labels.clone());
510        // Note we only return the tree ids here, not the labels. We do that to increase
511        // the chances of finding a common dominator value that is closer to the
512        // divergent commits, ideally one that result in a simple merge of the trees
513        // later on.
514        Ok(tree_ids_and_labels.tree_ids.clone())
515    };
516
517    let mut value_cache = ValueCache::new(async |commit_id: &CommitId| {
518        let commit = repo.store().get_commit_async(commit_id).await?;
519        value_fn(&commit).await
520    });
521    // Calculate the dominator value on the value flow graph, and record which
522    // commits produce which values.
523    let dominator_value = truncated_evolution_graph
524        .flow_graph
525        .find_dominator_value_with_value_cache(
526            truncated_evolution_graph.divergent_commit_ids(),
527            &mut value_cache,
528        )
529        .await
530        .map_err(|e| ConvergeError::Other(e.into()))?;
531    let dominator_producer =
532        get_value_producer(truncated_evolution_graph, &dominator_value, &value_cache)?;
533
534    let base_commit = repo.store().get_commit_async(&dominator_producer).await?;
535    let rebased_resolved_trees = Arc::try_unwrap(rebased_resolved_trees)
536        .map_err(|_| ConvergeError::Other("Failed to unwrap rebased_resolved_trees Arc".into()))?
537        .into_inner()
538        .unwrap();
539
540    let mut terms: Vec<(MergedTree, String)> = Vec::new();
541    let base_term = get_term_for_tree_merge(
542        &base_commit,
543        parents,
544        &rebased_resolved_trees,
545        "converge base",
546    );
547
548    // Add
549    terms.push(base_term.clone());
550    for divergent_commit in truncated_evolution_graph.divergent_commits() {
551        // Remove
552        terms.push(base_term.clone());
553        // Add
554        terms.push(get_term_for_tree_merge(
555            divergent_commit,
556            parents,
557            &rebased_resolved_trees,
558            "divergent commit",
559        ));
560    }
561    Ok(MergedTree::merge(MergeBuilder::from_iter(terms).build()).await?)
562}
563
564fn get_term_for_tree_merge(
565    commit: &Commit,
566    parents: &[CommitId],
567    rebased_resolved_trees: &HashMap<CommitId, TreeIdsAndLabels>,
568    prefix: &str,
569) -> (MergedTree, String) {
570    let rebased_and_resolved_tree = rebased_resolved_trees
571        .get(commit.id())
572        .unwrap()
573        .to_merged_tree(commit.store());
574    let conflict_label = if commit.parent_ids() == parents {
575        format!("{prefix}: {}", commit.conflict_label())
576    } else {
577        format!(
578            "{prefix}: tree of {} rebased onto parents",
579            commit.conflict_label()
580        )
581    };
582    (rebased_and_resolved_tree, conflict_label)
583}
584
585// Creates a merge of values, using as terms the values of the divergent
586// commits, and as base the dominator value. Returns the merge together with the
587// commit id of one of the commits that produces the dominator value. Commits in
588// excluded_divergent_commits are not used in the merge.
589async fn create_value_merge<T, VF>(
590    graph: &TruncatedEvolutionGraph,
591    excluded_divergent_commits: &HashSet<CommitId>,
592    value_fn: VF,
593) -> Result<(Merge<T>, CommitId), ConvergeError>
594where
595    T: Eq + Hash + Clone,
596    VF: AsyncFn(&Commit) -> Result<T, ConvergeError>,
597{
598    let mut value_cache = ValueCache::new(async |commit_id: &CommitId| {
599        let commit = graph.repo().store().get_commit_async(commit_id).await?;
600        value_fn(&commit).await
601    });
602
603    let divergent_commits = graph
604        .divergent_commit_ids()
605        .iter()
606        .filter(|id| !excluded_divergent_commits.contains(*id));
607
608    // Calculate the dominator value on the value flow graph, and record which
609    // commits produce which values.
610    let dominator_value = graph
611        .flow_graph
612        .find_dominator_value_with_value_cache(divergent_commits.clone(), &mut value_cache)
613        .await
614        .map_err(|e| ConvergeError::Other(e.into()))?;
615    let dominator_producer = get_value_producer(graph, &dominator_value, &value_cache)?;
616
617    let mut merge_builder = MergeBuilder::default();
618    // ADD
619    merge_builder.extend([(*dominator_value).clone()]);
620    for divergent_commit in divergent_commits {
621        let commit_value = value_cache.get_value(divergent_commit).await?;
622        // REMOVE, ADD
623        merge_builder.extend([(*dominator_value).clone(), (*commit_value).clone()]);
624    }
625    Ok((merge_builder.build(), dominator_producer))
626}
627
628/// Returns a commit that produces a given value (e.g. finds a commit that
629/// produces a given description). The value must be present in value_cache.
630fn get_value_producer<T, VF>(
631    truncated_evolution_graph: &TruncatedEvolutionGraph,
632    value: &Rc<T>,
633    value_cache: &ValueCache<CommitId, T, VF>,
634) -> Result<CommitId, ConvergeError>
635where
636    T: Eq + Hash,
637    VF: AsyncFn(&CommitId) -> Result<T, ConvergeError>,
638{
639    let producers = value_cache.get_nodes_for_value(value).unwrap();
640    match producers.len() {
641        0 => unreachable!(), // If it is present in ValueCache, it comes from some commit.
642        1 => return Ok(producers[0].clone()),
643        _ => {}
644    }
645
646    // If there is more than one producer we choose the one of minimum rank, where
647    // rank is defined as lowest change-offset. Because some backends may not
648    // provide change-offsets for hidden commits, we consider those as having
649    // maximum change-offset and use input-order as the secondary sorting criterion.
650    // By input-order we refer to the order of commits passed to converge_change.
651    // But some commits are not given as input, so we use CommitId as tertiary
652    // sorting criterion.
653
654    let resolved_change_targets = truncated_evolution_graph
655        .repo()
656        .resolve_change_id(truncated_evolution_graph.change_id())?;
657    let input_position: HashMap<&CommitId, usize> = truncated_evolution_graph
658        .divergent_commit_ids()
659        .iter()
660        .enumerate()
661        .map(|(position, commit_id)| (commit_id, position))
662        .collect();
663    let producer = producers
664        .iter()
665        .min_by_key(|commit_id: &&CommitId| {
666            let change_offset = match &resolved_change_targets {
667                Some(change_targets) => change_targets.find_offset(commit_id).unwrap_or(usize::MAX),
668                None => usize::MAX,
669            };
670            let input_position = *input_position.get(commit_id).unwrap_or(&usize::MAX);
671            (change_offset, input_position, *commit_id)
672        })
673        .unwrap()
674        .clone();
675    Ok(producer)
676}
677
678async fn rebase_tree_onto_solution_parents(
679    c: &Commit,
680    parents: &[CommitId],
681    parents_merged_tree: &MergedTree,
682    repo: &Arc<ReadonlyRepo>,
683) -> BackendResult<MergedTree> {
684    if c.parent_ids() == parents {
685        return Ok(c.tree());
686    }
687    let mut terms: Vec<(MergedTree, String)> = Vec::new();
688    // Add
689    terms.push((
690        parents_merged_tree.clone(),
691        "converge solution parent(s)".to_string(),
692    ));
693    // Remove
694    terms.push((
695        c.parent_tree_no_resolve(repo.as_ref()).await?,
696        c.parents_conflict_label().await?,
697    ));
698    // Add
699    terms.push((c.tree(), c.conflict_label()));
700    MergedTree::merge(MergeBuilder::from_iter(terms).build()).await
701}
702
703/// Returns those commits in commit_ids that are not descendants of any other
704/// commit in commit_ids.
705pub async fn remove_descendants(
706    repo: &Arc<ReadonlyRepo>,
707    commit_ids: &[CommitId],
708) -> Result<HashSet<CommitId>, ConvergeError> {
709    if commit_ids.is_empty() {
710        return Ok(HashSet::default());
711    }
712    let revset_expression = Arc::new(RevsetExpression::Commits(commit_ids.to_vec())).roots();
713    let mut result = HashSet::with_capacity(commit_ids.len());
714    let mut stream = revset_expression.evaluate(repo.deref())?.stream();
715    while let Some(commit_id) = stream.try_next().await? {
716        result.insert(commit_id);
717    }
718
719    validate(
720        !result.is_empty(),
721        &format!("the result of remove_descendants should never be empty; commits: {commit_ids:?}"),
722    )?;
723    Ok(result)
724}
725
726fn validate(predicate: bool, msg: &str) -> Result<(), ConvergeError> {
727    if !predicate {
728        Err(ConvergeError::Other(msg.into()))
729    } else {
730        Ok(())
731    }
732}