// Copyright 2025 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Utility for commit evolution history. use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::VecDeque; use std::slice; use itertools::Itertools as _; use thiserror::Error; use crate::backend::BackendError; use crate::backend::BackendResult; use crate::backend::CommitId; use crate::commit::Commit; use crate::dag_walk; use crate::op_store::OpStoreError; use crate::op_store::OpStoreResult; use crate::op_walk; use crate::operation::Operation; use crate::repo::ReadonlyRepo; use crate::repo::Repo as _; /// Commit with predecessor information. #[derive(Clone, Debug)] pub struct CommitEvolutionEntry { /// Commit id and metadata. pub commit: Commit, /// Operation where the commit was created or rewritten. pub operation: Option, /// Reachable predecessor ids reconstructed from the commit metadata. This /// should be set if the associated `operation` is unknown. // TODO: remove with legacy commit.predecessors support reachable_predecessors: Option>, } impl CommitEvolutionEntry { /// Predecessor ids of this commit. pub fn predecessor_ids(&self) -> &[CommitId] { match &self.operation { Some(op) => op.predecessors_for_commit(self.commit.id()).unwrap(), None => self.reachable_predecessors.as_ref().unwrap(), } } /// Predecessor commit objects of this commit. pub fn predecessors(&self) -> impl ExactSizeIterator> + use<'_> { let store = self.commit.store(); self.predecessor_ids().iter().map(|id| store.get_commit(id)) } } #[allow(missing_docs)] #[derive(Debug, Error)] pub enum WalkPredecessorsError { #[error(transparent)] Backend(#[from] BackendError), #[error(transparent)] OpStore(#[from] OpStoreError), #[error("Predecessors cycle detected around commit {0}")] CycleDetected(CommitId), } /// Walks operations to emit commit predecessors in reverse topological order. pub fn walk_predecessors<'repo>( repo: &'repo ReadonlyRepo, start_commits: &[CommitId], ) -> impl Iterator> + use<'repo> { WalkPredecessors { repo, op_ancestors: op_walk::walk_ancestors(slice::from_ref(repo.operation())), to_visit: start_commits.to_vec(), queued: VecDeque::new(), } } struct WalkPredecessors<'repo, I> { repo: &'repo ReadonlyRepo, op_ancestors: I, to_visit: Vec, queued: VecDeque, } impl WalkPredecessors<'_, I> where I: Iterator>, { fn try_next(&mut self) -> Result, WalkPredecessorsError> { while !self.to_visit.is_empty() && self.queued.is_empty() { let Some(op) = self.op_ancestors.next().transpose()? else { // Scanned all operations, no fallback needed. self.flush_commits()?; break; }; if !op.stores_commit_predecessors() { // There may be concurrent ops, but let's simply switch to the // legacy commit traversal. Operation history should be mostly // linear. self.scan_commits()?; break; } self.visit_op(&op)?; } Ok(self.queued.pop_front()) } /// Looks for predecessors within the given operation. fn visit_op(&mut self, op: &Operation) -> Result<(), WalkPredecessorsError> { let mut to_emit = Vec::new(); // transitive edges should be short let mut has_dup = false; let mut i = 0; while let Some(cur_id) = self.to_visit.get(i) { if let Some(next_ids) = op.predecessors_for_commit(cur_id) { if to_emit.contains(cur_id) { self.to_visit.remove(i); has_dup = true; continue; } to_emit.extend(self.to_visit.splice(i..=i, next_ids.iter().cloned())); } else { i += 1; } } let store = self.repo.store(); let mut emit = |id: &CommitId| -> BackendResult<()> { let commit = store.get_commit(id)?; self.queued.push_back(CommitEvolutionEntry { commit, operation: Some(op.clone()), reachable_predecessors: None, }); Ok(()) }; match &*to_emit { [] => {} [id] if !has_dup => emit(id)?, _ => { let sorted_ids = dag_walk::topo_order_reverse_ok( to_emit.iter().map(Ok), |&id| id, |&id| op.predecessors_for_commit(id).into_iter().flatten().map(Ok), |id| id, // Err(&CommitId) if graph has cycle ) .map_err(|id| WalkPredecessorsError::CycleDetected(id.clone()))?; for &id in &sorted_ids { if op.predecessors_for_commit(id).is_some() { emit(id)?; } } } } Ok(()) } /// Traverses predecessors from remainder commits. fn scan_commits(&mut self) -> BackendResult<()> { let store = self.repo.store(); let index = self.repo.index(); let mut commit_predecessors: HashMap> = HashMap::new(); let commits = dag_walk::topo_order_reverse_ok( self.to_visit.drain(..).map(|id| store.get_commit(&id)), |commit: &Commit| commit.id().clone(), |commit: &Commit| { let ids = commit_predecessors .entry(commit.id().clone()) .or_insert_with(|| { commit .store_commit() .predecessors .iter() // exclude unreachable predecessors .filter(|id| index.has_id(id)) .cloned() .collect() }); ids.iter().map(|id| store.get_commit(id)).collect_vec() }, |_| panic!("graph has cycle"), )?; self.queued.extend(commits.into_iter().map(|commit| { let predecessors = commit_predecessors .remove(commit.id()) .expect("commit must be visited once"); CommitEvolutionEntry { commit, operation: None, reachable_predecessors: Some(predecessors), } })); Ok(()) } /// Moves remainder commits to output queue. fn flush_commits(&mut self) -> BackendResult<()> { self.queued.reserve(self.to_visit.len()); for id in self.to_visit.drain(..) { let commit = self.repo.store().get_commit(&id)?; self.queued.push_back(CommitEvolutionEntry { commit, operation: None, // There were no legacy operations, so the commit should have no // predecessors. reachable_predecessors: Some(vec![]), }); } Ok(()) } } impl Iterator for WalkPredecessors<'_, I> where I: Iterator>, { type Item = Result; fn next(&mut self) -> Option { self.try_next().transpose() } } /// Collects predecessor records from `new_ops` to `old_ops`, and resolves /// transitive entries. /// /// This function assumes that there exists a single greatest common ancestors /// between `old_ops` and `new_ops`. If `old_ops` and `new_ops` have ancestors /// and descendants each other, or if criss-crossed merges exist between these /// operations, the returned mapping would be lossy. pub fn accumulate_predecessors( new_ops: &[Operation], old_ops: &[Operation], ) -> Result>, WalkPredecessorsError> { if new_ops.is_empty() || old_ops.is_empty() { return Ok(BTreeMap::new()); // No common ancestor exists } // Fast path for the single forward operation case. if let [op] = new_ops { if op.parent_ids().iter().eq(old_ops.iter().map(|op| op.id())) { let Some(map) = &op.store_operation().commit_predecessors else { return Ok(BTreeMap::new()); }; return resolve_transitive_edges(map, map.keys()) .map_err(|id| WalkPredecessorsError::CycleDetected(id.clone())); } } // Follow reverse edges from the common ancestor to old_ops. Here we use // BTreeMap to stabilize order of the reversed edges. let mut accumulated = BTreeMap::new(); let reverse_ops = op_walk::walk_ancestors_range(old_ops, new_ops); if !try_collect_predecessors_into(&mut accumulated, reverse_ops)? { return Ok(BTreeMap::new()); } let mut accumulated = reverse_edges(accumulated); // Follow forward edges from new_ops to the common ancestor. let forward_ops = op_walk::walk_ancestors_range(new_ops, old_ops); if !try_collect_predecessors_into(&mut accumulated, forward_ops)? { return Ok(BTreeMap::new()); } let new_commit_ids = new_ops .iter() .filter_map(|op| op.store_operation().commit_predecessors.as_ref()) .flat_map(|map| map.keys()); resolve_transitive_edges(&accumulated, new_commit_ids) .map_err(|id| WalkPredecessorsError::CycleDetected(id.clone())) } fn try_collect_predecessors_into( collected: &mut BTreeMap>, ops: impl IntoIterator>, ) -> OpStoreResult { for op in ops { let op = op?; let Some(map) = &op.store_operation().commit_predecessors else { return Ok(false); }; // Just insert. There should be no duplicate entries. collected.extend(map.iter().map(|(k, v)| (k.clone(), v.clone()))); } Ok(true) } /// Resolves transitive edges in `graph` starting from the `start` nodes, /// returns new DAG. The returned DAG only includes edges reachable from the /// `start` nodes. fn resolve_transitive_edges<'a: 'b, 'b>( graph: &'a BTreeMap>, start: impl IntoIterator, ) -> Result>, &'b CommitId> { let mut new_graph: BTreeMap> = BTreeMap::new(); let sorted_ids = dag_walk::topo_order_forward_ok( start.into_iter().map(Ok), |&id| id, |&id| graph.get(id).into_iter().flatten().map(Ok), |id| id, // Err(&CommitId) if graph has cycle )?; for cur_id in sorted_ids { let Some(neighbors) = graph.get(cur_id) else { continue; }; let lookup = |id| new_graph.get(id).map_or(slice::from_ref(id), Vec::as_slice); let new_neighbors = match &neighbors[..] { [id] => lookup(id).to_vec(), // unique() not needed ids => ids.iter().flat_map(lookup).unique().cloned().collect(), }; new_graph.insert(cur_id.clone(), new_neighbors); } Ok(new_graph) } fn reverse_edges(graph: BTreeMap>) -> BTreeMap> { let mut new_graph: BTreeMap> = BTreeMap::new(); for (node1, neighbors) in graph { for node2 in neighbors { new_graph.entry(node2).or_default().push(node1.clone()); } } new_graph }