use std::path::{Path, PathBuf};
use serde_json::{json, Map, Value};
use onevcs::{
Error, EventKind, Identity, PreservedBranch, Provenance, Recoverable, Result, Scope, Session,
SessionRequest, SessionToken, Vcs,
};
use crate::events::{self, Emission};
use crate::state::{self, VcsState};
use crate::store::{FileStore, MemoryStore, Store};
pub const DEFAULT_BASE: &str = "main";
#[derive(Debug)]
pub struct Repository<T> {
store: T,
root: PathBuf,
trees: Trees,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Trees {
Named,
Created,
}
pub type MemoryVcs = Repository<MemoryStore<VcsState>>;
pub type FileVcs = Repository<FileStore<VcsState>>;
impl MemoryVcs {
pub fn new() -> Self {
Self::seeded(VcsState::default())
}
pub fn seeded(state: VcsState) -> Self {
Self {
store: MemoryStore::new(state),
root: std::env::temp_dir().join("onevcs-testing-memory"),
trees: Trees::Named,
}
}
pub fn state(&self) -> VcsState {
self.store
.snapshot()
.expect("an in-memory store always answers")
}
}
impl Default for MemoryVcs {
fn default() -> Self {
Self::new()
}
}
impl FileVcs {
pub fn create(path: impl Into<PathBuf>) -> Result<Self> {
Self::over(FileStore::attach(path, &VcsState::default())?)
}
pub fn seeded(path: impl Into<PathBuf>, state: VcsState) -> Result<Self> {
Self::over(FileStore::replace(path, &state)?)
}
fn over(store: FileStore<VcsState>) -> Result<Self> {
let root = store
.path()
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.join("worktrees");
Ok(Self {
store,
root,
trees: Trees::Created,
})
}
pub fn state(&self) -> Result<VcsState> {
self.store.snapshot()
}
}
impl<T: Store<VcsState>> Vcs for Repository<T> {
fn resolve_identity(&self, origin_or_path: &str) -> Result<Identity> {
self.store.with(|state| {
state::identity_of(state, origin_or_path)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!(
"{origin_or_path:?} does not name a repository this provider knows; {}",
state::known(state)
),
})
})
}
fn open_session(&self, req: SessionRequest) -> Result<Session> {
let root = self.root.clone();
let (session, emission) = self.store.with(|state| {
let identity = state::identity_of(state, &req.repo)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!(
"{:?} does not name a repository this provider knows; {}",
req.repo,
state::known(state)
),
})?;
let token = SessionToken(format!("s-testing-{}", state.sessions.len() + 1));
let run_root = root.join(&token.0);
let base = req.base.clone().unwrap_or_else(|| DEFAULT_BASE.to_owned());
state::named_branch(&base, "the base")?;
let session = Session {
worktree: run_root.join("worktree"),
branch: state::requested_branch(&req, &token)?,
base,
token: token.clone(),
};
state.sessions.push(session.clone());
state
.session_identities
.insert(token.clone(), identity.origin.clone());
let emission = Emission {
stream: token.0.clone(),
identity: Some(identity.origin.clone()),
kind: EventKind::SessionOpened,
payload: object(json!({
"token": token.0,
"identity": identity.origin,
"branch": session.branch,
"base": session.base,
"worktree": session.worktree.display().to_string(),
"clone": run_root.join("clone").display().to_string(),
"execution_checkout": run_root.join("checkout").display().to_string(),
"publication_checkout": run_root.join("checkout").display().to_string(),
})),
};
Ok((session, emission))
})?;
if self.trees == Trees::Created {
std::fs::create_dir_all(&session.worktree).map_err(|e| Error::Invalid {
reason: format!("cannot create {}: {e}", session.worktree.display()),
})?;
}
events::emit(&emission);
Ok(session)
}
fn adopt_session(&self, token: SessionToken) -> Result<Session> {
self.store.with(|state| {
state::session_of(state, &token)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!(
"no session {:?} is open; `onevcs session open` prints a token",
token.0
),
})
})
}
fn preserve(&self, s: &Session, provenance: Provenance) -> Result<PreservedBranch> {
let (branch, emission) = self.store.with(|state| {
let identity = state
.session_identities
.get(&s.token)
.cloned()
.ok_or_else(|| Error::Invalid {
reason: format!(
"this provider has no record of session {:?}, so it cannot say which \
identity a branch preserved from it belongs to",
s.token.0
),
})?;
let branch = PreservedBranch {
branch: s.branch.clone(),
base: s.base.clone(),
provenance,
change_url: None,
change_base: None,
};
let row = Recoverable {
identity: identity.clone(),
branch: branch.clone(),
checkout: s.worktree.clone(),
stopped_because: format!("session {} was left open", s.token.0),
recover_command: recover_command(&s.branch, &s.worktree, provenance),
};
state.preserved.retain(|kept| {
kept.identity != row.identity || kept.branch.branch != row.branch.branch
});
state.preserved.push(row);
let emission = Emission {
stream: s.token.0.clone(),
identity: None,
kind: EventKind::CommitPreserved,
payload: object(json!({
"branch": s.branch,
"sha": events::stable_sha(&[&s.token.0, &s.branch, spell(provenance)]),
"provenance": spell(provenance),
})),
};
Ok((branch, emission))
})?;
events::emit(&emission);
Ok(branch)
}
fn recoverable(&self, scope: Scope) -> Result<Vec<Recoverable>> {
self.store.with(|state| {
let wanted = match &scope {
Scope::All => None,
Scope::Repo(repo) => Some(
state::identity_of(state, repo)
.map(|identity| identity.origin.clone())
.ok_or_else(|| Error::Invalid {
reason: format!(
"{repo:?} does not name a repository this provider knows; {}",
state::known(state)
),
})?,
),
};
Ok(state
.preserved
.iter()
.rev()
.filter(|row| wanted.as_ref().is_none_or(|key| *key == row.identity))
.cloned()
.collect())
})
}
}
fn recover_command(branch: &str, checkout: &Path, provenance: Provenance) -> Vec<String> {
match provenance {
Provenance::IncompleteStep => vec![
"onevcs".to_owned(),
"recover".to_owned(),
branch.to_owned(),
"--repo".to_owned(),
checkout.display().to_string(),
],
Provenance::Complete => vec![
"onevcs".to_owned(),
"integrate".to_owned(),
branch.to_owned(),
],
}
}
fn spell(provenance: Provenance) -> &'static str {
match provenance {
Provenance::Complete => "complete",
Provenance::IncompleteStep => "incomplete-step",
}
}
fn object(value: Value) -> Map<String, Value> {
value.as_object().cloned().unwrap_or_default()
}