// Copyright 2021 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. #![allow(missing_docs)] use std::cmp::min; use std::cmp::Ordering; use std::collections::BTreeMap; use std::collections::VecDeque; use super::bit_set::PositionsBitSet; use super::composite::CompositeIndex; use super::entry::CommitIndexEntry; use super::entry::GlobalCommitPosition; use super::rev_walk::RevWalk; use super::revset_engine::BoxedRevWalk; use crate::backend::CommitId; use crate::graph::GraphEdge; use crate::graph::GraphEdgeType; use crate::graph::GraphNode; use crate::revset::RevsetEvaluationError; // This can be cheaply allocated and hashed compared to `CommitId`-based type. type CommitGraphEdge = GraphEdge; /// Given a `RevWalk` over some set of revisions, yields the same revisions with /// associated edge types. /// /// If a revision's parent is in the input set, then the edge will be "direct". /// Otherwise, there will be one "indirect" edge for each closest ancestor in /// the set, and one "missing" edge for each edge leading outside the set. /// /// Example (uppercase characters are in the input set): /// /// A A /// |\ |\ /// B c B : /// |\| => |\: /// d E ~ E /// |/ ~ /// root /// /// The implementation works by walking the input set one commit at a time. It /// then considers all parents of the commit. It looks ahead in the input set /// far enough that all the parents will have been consumed if they are in the /// input (and puts them away so we can emit them later). If a parent of the /// current commit is not in the input set (i.e. it was not in the look-ahead), /// we walk these external commits until we end up back back in the input set. /// That walk may result in consuming more elements from the input `RevWalk`. /// In the example above, when we consider "A", we will initially look ahead to /// "B" and "c". When we consider edges from the external commit "c", we will /// further consume the input `RevWalk` to "E". /// /// Missing edges are those that don't lead back into the input set. If all /// edges from an external commit are missing, we consider the edge to that /// commit to also be missing. In the example above, that means that "B" will /// have a missing edge to "d" rather than to the root. /// /// `RevsetGraphWalk` can be configured to skip transitive edges that it would /// otherwise return. In this mode (which is the default), the edge from "A" to /// "E" in the example above would be excluded because there's also a transitive /// path from "A" to "E" via "B". The implementation of that mode /// adds a filtering step just before yielding the edges for a commit. The /// filtering works by doing a DFS in the simplified graph. That may require /// even more look-ahead. Consider this example (uppercase characters are in the /// input set): /// /// J /// /| /// | i /// | |\ /// | | H /// G | | /// | e f /// | \|\ /// | D | /// \ / c /// b / /// |/ /// A /// | /// root /// /// When walking from "J", we'll find indirect edges to "H", "G", and "D". This /// is our unfiltered set of edges, before removing transitive edges. In order /// to know that "D" is an ancestor of "H", we need to also walk from "H". We /// use the same search for finding edges from "H" as we used from "J". That /// results in looking ahead all the way to "A". We could reduce the amount of /// look-ahead by stopping at "c" since we're only interested in edges that /// could lead to "D", but that would require extra book-keeping to remember for /// later that the edges from "f" and "H" are only partially computed. pub(super) struct RevsetGraphWalk<'a> { input_set_walk: BoxedRevWalk<'a>, /// Commits in the input set we had to take out of the `RevWalk` while /// walking external edges. Does not necessarily include the commit /// we're currently about to emit. look_ahead: VecDeque, /// The last consumed position. This is always the smallest key in the /// look_ahead set, but it's faster to keep a separate field for it. min_position: GlobalCommitPosition, /// Edges for commits not in the input set. edges: BTreeMap>, skip_transitive_edges: bool, } impl<'a> RevsetGraphWalk<'a> { pub fn new(input_set_walk: BoxedRevWalk<'a>, skip_transitive_edges: bool) -> Self { RevsetGraphWalk { input_set_walk, look_ahead: VecDeque::new(), min_position: GlobalCommitPosition::MAX, edges: Default::default(), skip_transitive_edges, } } fn next_index_position( &mut self, index: &CompositeIndex, ) -> Result, RevsetEvaluationError> { match self.look_ahead.pop_back() { Some(position) => Ok(Some(position)), None => self.input_set_walk.next(index).transpose(), } } fn edges_from_internal_commit( &mut self, index: &CompositeIndex, index_entry: &CommitIndexEntry, ) -> Result<&[CommitGraphEdge], RevsetEvaluationError> { let position = index_entry.position(); // `if let Some(edges) = ...` doesn't pass lifetime check as of Rust 1.84.0 if self.edges.contains_key(&position) { return Ok(self.edges.get(&position).unwrap()); } let edges = self.new_edges_from_internal_commit(index, index_entry)?; Ok(self.edges.entry(position).or_insert(edges)) } fn pop_edges_from_internal_commit( &mut self, index: &CompositeIndex, index_entry: &CommitIndexEntry, ) -> Result, RevsetEvaluationError> { let position = index_entry.position(); while let Some(entry) = self.edges.last_entry() { match entry.key().cmp(&position) { Ordering::Less => break, // no cached edges found Ordering::Equal => return Ok(entry.remove()), Ordering::Greater => entry.remove(), }; } self.new_edges_from_internal_commit(index, index_entry) } fn new_edges_from_internal_commit( &mut self, index: &CompositeIndex, index_entry: &CommitIndexEntry, ) -> Result, RevsetEvaluationError> { let mut edges = Vec::new(); let mut known_ancestors = PositionsBitSet::with_max_pos(index_entry.position()); for parent in index_entry.parents() { let parent_position = parent.position(); self.consume_to(index, parent_position)?; if self.look_ahead.binary_search(&parent_position).is_ok() { edges.push(CommitGraphEdge::direct(parent_position)); } else { let parent_edges = self.edges_from_external_commit(index, parent)?; if parent_edges .iter() .all(|edge| edge.edge_type == GraphEdgeType::Missing) { edges.push(CommitGraphEdge::missing(parent_position)); } else { edges.extend( parent_edges .iter() .filter(|edge| !known_ancestors.get_set(edge.target)), ); } } } Ok(edges) } fn edges_from_external_commit( &mut self, index: &CompositeIndex, index_entry: CommitIndexEntry<'_>, ) -> Result<&[CommitGraphEdge], RevsetEvaluationError> { let position = index_entry.position(); let mut stack = vec![index_entry]; while let Some(entry) = stack.last() { let position = entry.position(); if self.edges.contains_key(&position) { stack.pop().unwrap(); continue; } let mut edges = Vec::new(); let mut known_ancestors = PositionsBitSet::with_max_pos(position); let mut parents_complete = true; for parent in entry.parents() { let parent_position = parent.position(); self.consume_to(index, parent_position)?; if self.look_ahead.binary_search(&parent_position).is_ok() { // We have found a path back into the input set edges.push(CommitGraphEdge::indirect(parent_position)); } else if let Some(parent_edges) = self.edges.get(&parent_position) { if parent_edges .iter() .all(|edge| edge.edge_type == GraphEdgeType::Missing) { edges.push(CommitGraphEdge::missing(parent_position)); } else { edges.extend( parent_edges .iter() .filter(|edge| !known_ancestors.get_set(edge.target)), ); } } else if parent_position < self.min_position { // The parent is not in the input set edges.push(CommitGraphEdge::missing(parent_position)); } else { // The parent is not in the input set but it's somewhere in the range // where we have commits in the input set, so continue searching. stack.push(parent); parents_complete = false; } } if parents_complete { stack.pop().unwrap(); self.edges.insert(position, edges); } } Ok(self.edges.get(&position).unwrap()) } fn remove_transitive_edges( &mut self, index: &CompositeIndex, edges: Vec, ) -> Result, RevsetEvaluationError> { if !edges .iter() .any(|edge| edge.edge_type == GraphEdgeType::Indirect) { return Ok(edges); } let Some(max_position) = edges.iter().map(|edge| edge.target).max() else { return Ok(edges); }; let mut min_generation = u32::MAX; let mut initial_targets = PositionsBitSet::with_max_pos(max_position); let mut work = vec![]; // To start with, add the edges one step after the input edges. for edge in &edges { initial_targets.set(edge.target); if edge.edge_type != GraphEdgeType::Missing { assert!(self.look_ahead.binary_search(&edge.target).is_ok()); let entry = index.commits().entry_by_pos(edge.target); min_generation = min(min_generation, entry.generation_number()); work.extend_from_slice(self.edges_from_internal_commit(index, &entry)?); } } // Find commits reachable transitively and add them to the `unwanted` set. let mut unwanted = PositionsBitSet::with_max_pos(max_position); while let Some(edge) = work.pop() { if edge.edge_type == GraphEdgeType::Missing || edge.target < self.min_position { continue; } if unwanted.get_set(edge.target) { // Already visited continue; } if initial_targets.get(edge.target) { // Already visited continue; } assert!(self.look_ahead.binary_search(&edge.target).is_ok()); let entry = index.commits().entry_by_pos(edge.target); if entry.generation_number() < min_generation { continue; } work.extend_from_slice(self.edges_from_internal_commit(index, &entry)?); } let mut edges = edges; edges.retain(|edge| !unwanted.get(edge.target)); Ok(edges) } fn consume_to( &mut self, index: &CompositeIndex, pos: GlobalCommitPosition, ) -> Result<(), RevsetEvaluationError> { while pos < self.min_position { if let Some(next_position) = self.input_set_walk.next(index).transpose()? { self.look_ahead.push_front(next_position); self.min_position = next_position; } else { break; } } Ok(()) } fn try_next( &mut self, index: &CompositeIndex, ) -> Result>, RevsetEvaluationError> { let Some(position) = self.next_index_position(index)? else { return Ok(None); }; let entry = index.commits().entry_by_pos(position); let mut edges = self.pop_edges_from_internal_commit(index, &entry)?; if self.skip_transitive_edges { edges = self.remove_transitive_edges(index, edges)?; } let edges = edges .iter() .map(|edge| edge.map(|pos| index.commits().entry_by_pos(pos).commit_id())) .collect(); Ok(Some((entry.commit_id(), edges))) } } impl RevWalk for RevsetGraphWalk<'_> { type Item = Result, RevsetEvaluationError>; fn next(&mut self, index: &CompositeIndex) -> Option { self.try_next(index).transpose() } }