use std::path::Path;
use zenith_core::{KdlAdapter, KdlSource as _};
use zenith_session::adapter::{OsClock, OsFs, OsRng};
use zenith_session::{
Outcome, RecordOutcome, StorePaths, VersionMeta, VersionOutcome, current_content,
list_versions, reconcile, record_state, record_version, resolve_data_dir, resolve_version,
version_content,
};
pub struct Recorded {
pub bytes: Vec<u8>,
pub doc_id: String,
pub warning: Option<String>,
}
pub fn record_edit(content: &[u8], doc_path: &Path, op_kind: &str) -> Recorded {
let paths = match resolve_data_dir() {
Ok(data_dir) => StorePaths::new(data_dir),
Err(e) => {
return Recorded {
bytes: content.to_vec(),
doc_id: String::new(),
warning: Some(format!("history: resolve_data_dir failed: {}", e.message)),
};
}
};
record_edit_in(&paths, content, doc_path, op_kind)
}
pub fn record_edit_in(
paths: &StorePaths,
content: &[u8],
doc_path: &Path,
op_kind: &str,
) -> Recorded {
let fs = OsFs;
let clock = OsClock;
let rng = OsRng;
let mut doc = match KdlAdapter.parse(content) {
Ok(d) => d,
Err(e) => {
return Recorded {
bytes: content.to_vec(),
doc_id: String::new(),
warning: Some(format!("history: parse failed: {}", e.message)),
};
}
};
let reconciled = match reconcile(&fs, paths, &clock, &rng, doc.doc_id.as_deref(), doc_path) {
Ok(r) => r,
Err(e) => {
return Recorded {
bytes: content.to_vec(),
doc_id: doc.doc_id.unwrap_or_default(),
warning: Some(format!("history: reconcile failed: {}", e.message)),
};
}
};
let final_bytes: Vec<u8> = match reconciled.outcome {
Outcome::Minted | Outcome::Copied { .. } => {
doc.doc_id = Some(reconciled.doc_id.clone());
match KdlAdapter.format(&doc) {
Ok(b) => b,
Err(e) => {
return Recorded {
bytes: content.to_vec(),
doc_id: reconciled.doc_id,
warning: Some(format!("history: format failed: {}", e.message)),
};
}
}
}
Outcome::Matched | Outcome::Moved { .. } | Outcome::Adopted => content.to_vec(),
};
if let Err(e) = record_state(
&fs,
paths,
&clock,
&rng,
&reconciled.doc_id,
&final_bytes,
Some(op_kind),
) {
return Recorded {
bytes: final_bytes,
doc_id: reconciled.doc_id,
warning: Some(format!(
"history: record_state failed: {} (file will still be written)",
e.message
)),
};
}
if let Err(e) = record_version(
&fs,
paths,
&clock,
&reconciled.doc_id,
&final_bytes,
VersionMeta {
op_kind: Some(op_kind),
..Default::default()
},
) {
return Recorded {
bytes: final_bytes,
doc_id: reconciled.doc_id,
warning: Some(format!(
"history: record_version failed: {} (file will still be written)",
e.message
)),
};
}
Recorded {
bytes: final_bytes,
doc_id: reconciled.doc_id,
warning: None,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistoryLine {
pub id: String,
pub seq: u64,
pub label: Option<String>,
pub op_kind: Option<String>,
pub timestamp_ms: Option<u128>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistoryView {
pub doc_id: String,
pub versions: Vec<HistoryLine>,
pub has_session: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum NavOutcome {
Moved,
NothingToDo,
}
fn read_doc_with_id(doc_path: &Path) -> Result<(Vec<u8>, String), String> {
let bytes = std::fs::read(doc_path)
.map_err(|e| format!("cannot read '{}': {e}", doc_path.display()))?;
let doc = KdlAdapter
.parse(&bytes)
.map_err(|e| format!("cannot parse '{}': {}", doc_path.display(), e.message))?;
let id = doc.doc_id.ok_or_else(|| {
format!(
"'{}' has no history yet (no doc-id); edit it with `zenith tx --apply` or \
`zenith library add` first",
doc_path.display()
)
})?;
Ok((bytes, id))
}
fn doc_id_at(doc_path: &Path) -> Result<String, String> {
read_doc_with_id(doc_path).map(|(_, id)| id)
}
pub fn read_doc_id(doc_path: &Path) -> Result<String, String> {
doc_id_at(doc_path)
}
pub struct EnsuredDocId {
pub doc_id: String,
pub warning: Option<String>,
}
pub fn ensure_doc_id_in(paths: &StorePaths, doc_path: &Path) -> Result<EnsuredDocId, String> {
let bytes = std::fs::read(doc_path)
.map_err(|e| format!("cannot read '{}': {e}", doc_path.display()))?;
let parsed = KdlAdapter
.parse(&bytes)
.map_err(|e| format!("cannot parse '{}': {}", doc_path.display(), e.message))?;
if let Some(doc_id) = parsed.doc_id {
return Ok(EnsuredDocId {
doc_id,
warning: None,
});
}
let recorded = record_edit_in(paths, &bytes, doc_path, "document.attach");
std::fs::write(doc_path, &recorded.bytes)
.map_err(|e| format!("cannot write '{}': {e}", doc_path.display()))?;
if recorded.doc_id.is_empty() {
return Err(format!(
"failed to attach a doc-id to '{}' (no id present after recording)",
doc_path.display()
));
}
Ok(EnsuredDocId {
doc_id: recorded.doc_id,
warning: recorded.warning,
})
}
pub fn history_view(doc_path: &Path) -> Result<HistoryView, String> {
let data_dir = resolve_data_dir().map_err(|e| e.message)?;
let paths = StorePaths::new(data_dir);
history_view_in(&paths, doc_path)
}
pub fn history_view_in(paths: &StorePaths, doc_path: &Path) -> Result<HistoryView, String> {
let doc_id = doc_id_at(doc_path)?;
let fs = OsFs;
let versions = list_versions(&fs, paths, &doc_id)
.map_err(|e| e.message)?
.into_iter()
.map(|r| HistoryLine {
id: r.id,
seq: r.seq,
label: r.label,
op_kind: r.op_kind,
timestamp_ms: r.timestamp_ms,
})
.collect();
let has_session = current_content(&fs, paths, &doc_id)
.map_err(|e| e.message)?
.is_some();
Ok(HistoryView {
doc_id,
versions,
has_session,
})
}
pub fn undo_edit(doc_path: &Path) -> Result<NavOutcome, String> {
let data_dir = resolve_data_dir().map_err(|e| e.message)?;
let paths = StorePaths::new(data_dir);
undo_edit_in(&paths, doc_path)
}
pub fn undo_edit_in(paths: &StorePaths, doc_path: &Path) -> Result<NavOutcome, String> {
let doc_id = doc_id_at(doc_path)?;
let fs = OsFs;
match zenith_session::undo(&fs, paths, &doc_id).map_err(|e| e.message)? {
Some(content) => {
std::fs::write(doc_path, &content)
.map_err(|e| format!("cannot write '{}': {e}", doc_path.display()))?;
Ok(NavOutcome::Moved)
}
None => Ok(NavOutcome::NothingToDo),
}
}
pub fn redo_edit(doc_path: &Path) -> Result<NavOutcome, String> {
let data_dir = resolve_data_dir().map_err(|e| e.message)?;
let paths = StorePaths::new(data_dir);
redo_edit_in(&paths, doc_path)
}
pub fn redo_edit_in(paths: &StorePaths, doc_path: &Path) -> Result<NavOutcome, String> {
let doc_id = doc_id_at(doc_path)?;
let fs = OsFs;
match zenith_session::redo(&fs, paths, &doc_id).map_err(|e| e.message)? {
Some(content) => {
std::fs::write(doc_path, &content)
.map_err(|e| format!("cannot write '{}': {e}", doc_path.display()))?;
Ok(NavOutcome::Moved)
}
None => Ok(NavOutcome::NothingToDo),
}
}
pub fn name_version(doc_path: &Path, name: &str) -> Result<String, String> {
let data_dir = resolve_data_dir().map_err(|e| e.message)?;
let paths = StorePaths::new(data_dir);
name_version_in(&paths, doc_path, name)
}
pub fn name_version_in(paths: &StorePaths, doc_path: &Path, name: &str) -> Result<String, String> {
let (bytes, doc_id) = read_doc_with_id(doc_path)?;
let fs = OsFs;
let clock = OsClock;
match record_version(
&fs,
paths,
&clock,
&doc_id,
&bytes,
VersionMeta {
label: Some(name),
op_kind: Some("named"),
..Default::default()
},
) {
Ok(VersionOutcome::Recorded { id }) => Ok(id),
Ok(VersionOutcome::Unchanged) => {
resolve_version(&fs, paths, &doc_id, "@head").map_err(|e| e.message)
}
Err(e) => Err(e.message),
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SyncOutcome {
Captured { id: String },
AlreadyInSync,
}
pub fn sync_external(doc_path: &Path) -> Result<SyncOutcome, String> {
let data_dir = resolve_data_dir().map_err(|e| e.message)?;
let paths = StorePaths::new(data_dir);
sync_external_in(&paths, doc_path)
}
pub fn sync_external_in(paths: &StorePaths, doc_path: &Path) -> Result<SyncOutcome, String> {
let (bytes, doc_id) = read_doc_with_id(doc_path)?;
let fs = OsFs;
let clock = OsClock;
let rng = OsRng;
match record_state(&fs, paths, &clock, &rng, &doc_id, &bytes, Some("external")) {
Ok(RecordOutcome::Recorded { id }) => Ok(SyncOutcome::Captured { id }),
Ok(RecordOutcome::Unchanged) => Ok(SyncOutcome::AlreadyInSync),
Err(e) => Err(e.message),
}
}
pub struct RestoreOutcome {
pub version_id: String,
pub warning: Option<String>,
}
pub fn restore(doc_path: &Path, spec: &str) -> Result<RestoreOutcome, String> {
let data_dir = resolve_data_dir().map_err(|e| e.message)?;
let paths = StorePaths::new(data_dir);
restore_in(&paths, doc_path, spec)
}
pub fn restore_in(
paths: &StorePaths,
doc_path: &Path,
spec: &str,
) -> Result<RestoreOutcome, String> {
let doc_id = doc_id_at(doc_path)?;
let fs = OsFs;
let version_id = resolve_version(&fs, paths, &doc_id, spec).map_err(|e| e.message)?;
let content = version_content(&fs, paths, &doc_id, &version_id).map_err(|e| e.message)?;
let recorded = record_edit_in(paths, &content, doc_path, "restore");
std::fs::write(doc_path, &recorded.bytes)
.map_err(|e| format!("cannot write '{}': {e}", doc_path.display()))?;
Ok(RestoreOutcome {
version_id,
warning: recorded.warning,
})
}