use std::cell::RefCell;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::host::{MergedChange, Sha};
use crate::retire::{BranchHolderKind, Retirement};
use crate::{home, ids};
const FORMAT: u32 = 2;
const WRITER: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Key {
pub(crate) identity: String,
pub(crate) branch: String,
pub(crate) base: String,
pub(crate) base_tip: Sha,
pub(crate) copies: Vec<KeyedCopy>,
pub(crate) unreadable: Vec<String>,
pub(crate) records: String,
pub(crate) session: Option<String>,
pub(crate) landed_trailer: String,
pub(crate) asking: Asking,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Asking {
Host,
Records,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct KeyedCopy {
pub(crate) kind: BranchHolderKind,
pub(crate) location: String,
pub(crate) tip: Sha,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Reply<T> {
NotAsked,
Answered(T),
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct HostAnswer {
pub(crate) merged: Reply<Option<MergedChange>>,
pub(crate) open: Reply<ChangeState>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum ChangeState {
Open,
NotOpen,
}
impl HostAnswer {
pub(crate) fn not_asked() -> Self {
HostAnswer {
merged: Reply::NotAsked,
open: Reply::NotAsked,
}
}
pub(crate) fn failed(&self) -> bool {
matches!(self.merged, Reply::Failed) || matches!(self.open, Reply::Failed)
}
pub(crate) fn same_as(&self, other: &HostAnswer) -> bool {
let merged = |answer: &HostAnswer| match &answer.merged {
Reply::Answered(Some(merged)) => Some(merged.clone()),
_ => None,
};
merged(self) == merged(other) && self.open == other.open
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Verdict {
pub(crate) retirement: Retirement,
pub(crate) reached: Reached,
pub(crate) consulted: bool,
pub(crate) host: HostAnswer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Reached {
Early,
Concluded,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Record {
format: u32,
onevcs: String,
key: Key,
verdict: Verdict,
}
pub(crate) struct Store {
directory: PathBuf,
unwritten: RefCell<Vec<String>>,
}
impl Store {
pub(crate) fn open() -> Result<Self> {
Ok(Store {
directory: home::verdicts_dir()?,
unwritten: RefCell::new(Vec::new()),
})
}
fn path(&self, identity: &str, branch: &str) -> PathBuf {
self.directory.join(format!(
"{}.json",
ids::digest(&format!("{identity}\n{branch}"))
))
}
pub(crate) fn read(&self, key: &Key) -> Option<Verdict> {
let text = std::fs::read_to_string(self.path(&key.identity, &key.branch)).ok()?;
let record: Record = serde_json::from_str(&text).ok()?;
(record.format == FORMAT && record.onevcs == WRITER && record.key == *key)
.then_some(record.verdict)
}
pub(crate) fn write(&self, key: &Key, verdict: &Verdict) {
let record = Record {
format: FORMAT,
onevcs: WRITER.to_owned(),
key: key.clone(),
verdict: verdict.clone(),
};
let path = self.path(&key.identity, &key.branch);
let written = serde_json::to_string_pretty(&record)
.map_err(|failure| failure.to_string())
.and_then(|text| {
home::atomic_write(&path, &format!("{text}\n")).map_err(|e| e.to_string())
});
if let Err(failure) = written {
self.unwritten
.borrow_mut()
.push(format!("{} of {}: {failure}", key.branch, key.identity));
}
}
pub(crate) fn forget(&self, identity: &str, branch: &str) {
let _ = std::fs::remove_file(self.path(identity, branch));
}
pub(crate) fn report(&self) {
let unwritten = self.unwritten.borrow();
if let Some(first) = unwritten.first() {
eprintln!(
"onevcs: warning: {count} branch verdict(s) could not be recorded under {dir}, so \
the next pass derives them again; this pass is complete. The first: {first}",
count = unwritten.len(),
dir = self.directory.display(),
);
}
}
}