use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use onevcs::{ChangeId, ChangeRequest, Check, Error, Identity, MergeOutcome, Recoverable, Result};
use onevcs::{Session, SessionRequest, SessionToken};
use crate::events;
use crate::store::Checked;
pub const STATE_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VcsState {
pub version: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub identities: Vec<Identity>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub sessions: Vec<Session>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub session_identities: BTreeMap<SessionToken, String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub preserved: Vec<Recoverable>,
}
impl Default for VcsState {
fn default() -> Self {
Self {
version: STATE_VERSION,
identities: Vec::new(),
sessions: Vec::new(),
session_identities: BTreeMap::new(),
preserved: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HostState {
pub version: u32,
pub authenticated_user: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub changes: Vec<ChangeRequest>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub heads: BTreeMap<ChangeId, String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub checks: BTreeMap<ChangeId, Vec<Check>>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub check_logs: BTreeMap<ChangeId, BTreeMap<String, String>>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub merges: BTreeMap<ChangeId, MergeOutcome>,
}
pub const DEFAULT_AUTHENTICATED_USER: &str = "onevcs-testing";
impl Default for HostState {
fn default() -> Self {
Self {
version: STATE_VERSION,
authenticated_user: DEFAULT_AUTHENTICATED_USER.to_owned(),
changes: Vec::new(),
heads: BTreeMap::new(),
checks: BTreeMap::new(),
check_logs: BTreeMap::new(),
merges: BTreeMap::new(),
}
}
}
pub(crate) fn identity_of<'a>(state: &'a VcsState, origin_or_path: &str) -> Option<&'a Identity> {
let wanted = origin_or_path.trim_end_matches('/');
state
.identities
.iter()
.find(|identity| identity.origin == wanted)
.or_else(|| {
state.identities.iter().find(|identity| {
identity
.origin
.rsplit('/')
.next()
.is_some_and(|name| name == wanted)
|| identity.origin.ends_with(&format!("/{wanted}"))
})
})
}
pub(crate) fn known(state: &VcsState) -> String {
if state.identities.is_empty() {
return "this provider was seeded with no identities".to_owned();
}
let names: Vec<&str> = state
.identities
.iter()
.map(|identity| identity.origin.as_str())
.collect();
format!("it knows {}", names.join(", "))
}
pub(crate) fn session_of<'a>(state: &'a VcsState, token: &SessionToken) -> Option<&'a Session> {
state
.sessions
.iter()
.find(|session| session.token == *token)
}
pub(crate) fn requested_branch(req: &SessionRequest, token: &SessionToken) -> Result<String> {
let name = req
.branch
.clone()
.unwrap_or_else(|| format!("onevcs/{}", token.0));
named_branch(&name, "the branch")?;
Ok(name)
}
pub(crate) fn named_branch(value: &str, what: &str) -> Result<()> {
let components_usable = !value.is_empty()
&& value.split('/').all(|component| {
!component.is_empty() && !component.starts_with('.') && !component.ends_with(".lock")
});
let usable = components_usable
&& !value.starts_with('-')
&& !value.contains("..")
&& !value.contains("@{")
&& !value.ends_with('.')
&& !value.ends_with('/')
&& !value.chars().any(|c| {
c.is_whitespace() || c.is_ascii_control() || c == '\u{7f}' || "~^:?*[\\".contains(c)
});
if !usable {
return Err(Error::Invalid {
reason: format!("{what} {value:?} is a name git would not accept"),
});
}
Ok(())
}
impl Checked for VcsState {
fn check(&self) -> Result<()> {
readable_version(self.version)?;
for session in &self.sessions {
if !events::is_safe_name(&session.token.0) {
return Err(Error::Invalid {
reason: format!("{:?} is not a session token", session.token.0),
});
}
named_branch(&session.branch, "the branch")?;
named_branch(&session.base, "the base")?;
}
for row in &self.preserved {
named_branch(&row.branch.branch, "the preserved branch")?;
named_branch(&row.branch.base, "the preserved branch's base")?;
}
Ok(())
}
}
impl Checked for HostState {
fn check(&self) -> Result<()> {
readable_version(self.version)?;
for change in &self.changes {
named_branch(&change.base, "the base of a seeded change request")?;
if change.id.0.is_empty() {
return Err(Error::Invalid {
reason: "a seeded change request carries no identifier".to_owned(),
});
}
if change.head_sha.0.trim().is_empty() {
return Err(Error::Invalid {
reason: format!(
"the seeded change request {:?} names no commit its checks are \
reported against",
change.id.0
),
});
}
}
for head in self.heads.values() {
named_branch(head, "the head of a seeded change request")?;
}
Ok(())
}
}
fn readable_version(declared: u32) -> Result<()> {
if declared != STATE_VERSION {
return Err(Error::Invalid {
reason: format!(
"the document declares version {declared}; this build reads version \
{STATE_VERSION}"
),
});
}
Ok(())
}