use crate::content::node::NodeState;
use crate::content::provider::SegmentProvider;
use crate::error::Result;
use crate::journal::read_journal;
use crate::progress::{DiscardedProgress, ProgressObserver, Step, WorkUnit};
use crate::segment::record::RecordIdentifier;
use crate::store::{ArchiveSet, open_all_archives_with_progress};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NodeHistoryEntry {
pub revision: String,
pub timestamp_milliseconds: i64,
pub record: Option<RecordIdentifier>,
}
pub fn node_history(directory: &std::path::Path, path: &str) -> Result<Vec<NodeHistoryEntry>> {
node_history_with_progress(directory, path, &mut DiscardedProgress)
}
pub fn node_history_with_progress(
directory: &std::path::Path,
path: &str,
observer: &mut dyn ProgressObserver,
) -> Result<Vec<NodeHistoryEntry>> {
let archives = open_all_archives_with_progress(directory, observer)?;
let provider = ArchiveSet::new(archives);
let journal_entries = read_journal(&directory.join("journal.log"))?;
let mut history = Vec::new();
observer.step_began(
&Step::new("tracing revisions", WorkUnit::Revisions)
.with_total(crate::progress::count(journal_entries.len())),
);
for (traced, entry) in journal_entries.iter().enumerate() {
observer.step_advanced(crate::progress::count(traced));
let Some(head) = entry.record_identifier() else {
continue;
};
if provider.segment(head.segment).is_err() {
continue;
}
let record = match navigate(&provider, head, path) {
Ok(node) => node.map(|node| node.record_identifier()),
Err(error) => {
observer.step_ended();
return Err(error);
}
};
history.push(NodeHistoryEntry {
revision: entry.revision_text.clone(),
timestamp_milliseconds: entry.timestamp_milliseconds,
record,
});
}
observer.step_advanced(crate::progress::count(journal_entries.len()));
observer.step_ended();
Ok(history)
}
fn navigate<'provider>(
provider: &'provider ArchiveSet,
head: RecordIdentifier,
path: &str,
) -> Result<Option<NodeState<'provider>>> {
let super_root = NodeState::new(provider, head);
let segments: Vec<&str> = path
.split('/')
.filter(|segment| !segment.is_empty())
.collect();
let (mut current, rest) = if segments.first() == Some(&"checkpoints") {
(super_root, &segments[..])
} else {
match super_root.child_node("root")? {
Some(root) => (root, &segments[..]),
None => return Ok(None),
}
};
for name in rest {
match current.child_node(name)? {
Some(child) => current = child,
None => return Ok(None),
}
}
Ok(Some(current))
}
#[cfg(test)]
mod tests {
use super::node_history;
use crate::writer::record_writer::{ChildNodesToWrite, PropertyToWrite, PropertyValuesToWrite};
use crate::writer::store_writer::WritableRepository;
struct TestDirectory {
path: std::path::PathBuf,
}
impl TestDirectory {
fn new(name: &str) -> Self {
let path =
std::env::temp_dir().join(format!("froe-history-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
Self { path }
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn write_revision(directory: &std::path::Path, title: &str) {
let store = WritableRepository::open(directory).expect("open");
let generation = store.writing_generation().expect("generation");
let mut writer = store.record_writer(generation);
let value = writer.write_string(title).expect("value");
let content = writer
.write_node(
Some("nt:unstructured"),
&[],
&ChildNodesToWrite::Zero,
&[PropertyToWrite {
name: "title".to_owned(),
property_type: crate::content::property::PropertyType::String,
values: PropertyValuesToWrite::Single(value),
}],
)
.expect("content");
let root = writer
.write_node(
None,
&[],
&ChildNodesToWrite::One {
name: "content".to_owned(),
node: content,
},
&[],
)
.expect("root");
let head = writer
.write_node(
None,
&[],
&ChildNodesToWrite::One {
name: "root".to_owned(),
node: root,
},
&[],
)
.expect("super root");
writer.finish().expect("finish");
let previous = store.head();
assert!(store.compare_and_set_head(previous, head));
store.close().expect("close");
}
#[test]
fn tracks_a_node_across_revisions() {
let directory = TestDirectory::new("track");
write_revision(&directory.path, "first");
write_revision(&directory.path, "second");
write_revision(&directory.path, "third");
let history = node_history(&directory.path, "/content").expect("history");
assert!(history.len() >= 3);
assert!(
history[0].record.is_some(),
"the newest revision has the content node"
);
let distinct: std::collections::HashSet<_> =
history.iter().filter_map(|entry| entry.record).collect();
assert!(
distinct.len() >= 3,
"each revision has a distinct content record"
);
}
#[test]
fn missing_paths_are_recorded_as_absent() {
let directory = TestDirectory::new("absent");
write_revision(&directory.path, "only");
let history = node_history(&directory.path, "/does-not-exist").expect("history");
assert!(history.iter().all(|entry| entry.record.is_none()));
}
}