use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use onevcs::{
ChangeId, ChangeRequest, Check, CheckSource, Error, Identity, MergeOutcome, Recoverable, Result,
};
use onevcs::{MergePolicy, Publication, Session, SessionRequest, SessionToken};
use crate::events;
use crate::store::Checked;
pub const STATE_VERSION: u32 = 2;
#[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>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub closed_sessions: BTreeSet<SessionToken>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy: Option<MergePolicy>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub publications: Vec<Publication>,
}
impl Default for VcsState {
fn default() -> Self {
Self {
version: STATE_VERSION,
identities: Vec::new(),
sessions: Vec::new(),
session_identities: BTreeMap::new(),
preserved: Vec::new(),
closed_sessions: BTreeSet::new(),
policy: None,
publications: 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 titles: 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 = "Option::is_none")]
pub check_sources: Option<BTreeSet<CheckSource>>,
#[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(),
titles: BTreeMap::new(),
checks: BTreeMap::new(),
check_logs: BTreeMap::new(),
check_sources: None,
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 identity_for(state: &VcsState, token: &SessionToken) -> Result<String> {
state
.session_identities
.get(token)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!(
"this provider has no record of session {:?}, so it cannot say which identity \
its work belongs to",
token.0
),
})
}
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 {
known_identity(self, &row.identity, "preserved work")?;
named_branch(&row.branch.branch, "the preserved branch")?;
named_branch(&row.branch.base, "the preserved branch's base")?;
}
for token in &self.closed_sessions {
opened(self, token, "closed")?;
}
for (token, origin) in &self.session_identities {
opened(self, token, "given an identity")?;
known_identity(self, origin, &format!("session {:?}", token.0))?;
}
for publication in &self.publications {
let session = opened(self, &publication.session, "published")?;
named_branch(&publication.branch, "the published branch")?;
if publication.branch != session.branch {
return Err(Error::Invalid {
reason: format!(
"the publication of session {:?} names branch {:?}, but that session is \
on {:?}",
publication.session.0, publication.branch, session.branch
),
});
}
}
Ok(())
}
}
fn opened_change(state: &HostState, id: &ChangeId, what: &str) -> Result<()> {
if state.changes.iter().any(|change| change.id == *id) {
return Ok(());
}
Err(Error::Invalid {
reason: format!(
"{what} is recorded for change request {:?}, but no change request by that \
identifier was opened",
id.0
),
})
}
fn known_identity(state: &VcsState, origin: &str, what: &str) -> Result<()> {
if state
.identities
.iter()
.any(|identity| identity.origin == origin)
{
return Ok(());
}
Err(Error::Invalid {
reason: format!(
"{what} belongs to identity {origin:?}, which this provider does not know; {}",
known(state)
),
})
}
pub(crate) fn titled(title: &str) -> Result<()> {
if title.trim().is_empty() {
return Err(Error::Invalid {
reason: "a change request's title is blank, so it names no change".to_owned(),
});
}
Ok(())
}
fn opened<'a>(state: &'a VcsState, token: &SessionToken, what: &str) -> Result<&'a Session> {
if !events::is_safe_name(&token.0) {
return Err(Error::Invalid {
reason: format!("{:?} is not a session token", token.0),
});
}
session_of(state, token).ok_or_else(|| Error::Invalid {
reason: format!(
"session {:?} is {what} here, but no session by that token was opened",
token.0
),
})
}
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 (id, head) in &self.heads {
opened_change(self, id, "a head")?;
named_branch(head, "the head of a seeded change request")?;
}
for (id, title) in &self.titles {
opened_change(self, id, "a title")?;
titled(title)?;
}
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(())
}