diff --git a/lib/src/index.rs b/lib/src/index.rs index 357088807..86f703cbe 100644 --- a/lib/src/index.rs +++ b/lib/src/index.rs @@ -149,6 +149,11 @@ impl HexPrefix { &self.min_prefix_bytes } + /// Returns the bytes representation if this prefix can be a full id. + pub fn as_full_bytes(&self) -> Option<&[u8]> { + (!self.has_odd_byte).then_some(&self.min_prefix_bytes) + } + fn split_odd_byte(&self) -> (Option, &[u8]) { if self.has_odd_byte { let (&odd, prefix) = self.min_prefix_bytes.split_last().unwrap(); diff --git a/lib/src/op_walk.rs b/lib/src/op_walk.rs index 7fc47061f..21b388573 100644 --- a/lib/src/op_walk.rs +++ b/lib/src/op_walk.rs @@ -15,13 +15,13 @@ //! Utility for operation id resolution and traversal. use std::cmp::Ordering; -use std::collections::{HashSet, VecDeque}; use std::sync::Arc; use itertools::Itertools as _; use thiserror::Error; use crate::backend::ObjectId as _; +use crate::index::HexPrefix; use crate::op_heads_store::{OpHeadResolutionError, OpHeadsStore}; use crate::op_store::{OpStore, OpStoreError, OpStoreResult, OperationId}; use crate::operation::Operation; @@ -126,11 +126,13 @@ fn resolve_single_op_from_store( op_heads_store: &dyn OpHeadsStore, op_str: &str, ) -> Result { - if op_str.is_empty() || !op_str.as_bytes().iter().all(|b| b.is_ascii_hexdigit()) { + if op_str.is_empty() { return Err(OpsetResolutionError::InvalidIdPrefix(op_str.to_owned()).into()); } - if let Ok(binary_op_id) = hex::decode(op_str) { - let op_id = OperationId::new(binary_op_id); + let prefix = HexPrefix::new(op_str) + .ok_or_else(|| OpsetResolutionError::InvalidIdPrefix(op_str.to_owned()))?; + if let Some(binary_op_id) = prefix.as_full_bytes() { + let op_id = OperationId::from_bytes(binary_op_id); match op_store.read_operation(&op_id) { Ok(operation) => { return Ok(Operation::new(op_store.clone(), op_id, operation)); @@ -143,12 +145,14 @@ fn resolve_single_op_from_store( } } } - let mut matches = vec![]; - for op in find_all_operations(op_store, op_heads_store)? { - if op.id().hex().starts_with(op_str) { - matches.push(op); - } - } + + // TODO: Extract to OpStore method where IDs can be resolved without loading + // all operation data? + let head_ops = get_current_head_ops(op_store, op_heads_store)?; + let mut matches: Vec<_> = walk_ancestors(&head_ops) + .filter_ok(|op| prefix.matches(op.id())) + .take(2) + .try_collect()?; if matches.is_empty() { Err(OpsetResolutionError::NoSuchOperation(op_str.to_owned()).into()) } else if matches.len() == 1 { @@ -158,22 +162,20 @@ fn resolve_single_op_from_store( } } -fn find_all_operations( +/// Loads the current head operations. The returned operations may contain +/// redundant ones which are ancestors of the other heads. +fn get_current_head_ops( op_store: &Arc, op_heads_store: &dyn OpHeadsStore, -) -> Result, OpStoreError> { - let mut visited = HashSet::new(); - let mut work: VecDeque<_> = op_heads_store.get_op_heads().into_iter().collect(); - let mut operations = vec![]; - while let Some(op_id) = work.pop_front() { - if visited.insert(op_id.clone()) { - let store_operation = op_store.read_operation(&op_id)?; - work.extend(store_operation.parents.iter().cloned()); - let operation = Operation::new(op_store.clone(), op_id, store_operation); - operations.push(operation); - } - } - Ok(operations) +) -> OpStoreResult> { + op_heads_store + .get_op_heads() + .into_iter() + .map(|id| -> OpStoreResult { + let data = op_store.read_operation(&id)?; + Ok(Operation::new(op_store.clone(), id, data)) + }) + .try_collect() } #[derive(Clone, Debug, Eq, Hash, PartialEq)]