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