//! Where a project's main lives. A local project's main is in this //! machine's database; a hosted project's main is on its host, reached //! over the vcs protocol (remote.rs). Commands only talk to [`Host`]. use anyhow::Result; use serde::{Deserialize, Serialize}; use vcs_core::merge::{self, Conflict, Mode}; use vcs_core::version::Version; use vcs_core::{Id, tree}; use crate::db::{Change, Db, Project}; use crate::ids; use crate::objects::Store; /// What landing a change asks for. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LandRequest { pub change: String, pub base_tree: Id, pub tree: Id, pub title: String, pub body: String, pub author: String, /// Allow something the secret check flagged (recorded). #[serde(default)] pub allow_secrets: bool, /// The change itself, recorded first (a hosted project's own change), so /// landing is one call. #[serde(default, skip_serializing_if = "Option::is_none")] pub record: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "outcome", rename_all = "snake_case")] pub enum Landed { Landed { version: Version }, Conflict { main: u64, conflicts: Vec }, Waiting { asked: Vec, reason: String }, ChecksFailed { check: String, log: Option }, } pub trait Host { /// Brings the local copy of main up to date and returns its latest /// version (None: the project has no versions yet). fn refresh(&self, db: &Db, store: &Store, project: &Project) -> Result>; /// Makes sure every object under `tree` is in the store. `have` names a /// tree whose objects are already here, to skip what they share. fn fetch_tree(&self, store: &Store, project: &Project, tree: &Id, have: Option<&Id>) -> Result<()>; fn land(&self, db: &Db, store: &Store, project: &Project, request: &LandRequest) -> Result; /// Fetches main's versions from `from` up to the first one this /// machine keeps (local projects keep them all). fn fetch_history(&self, _db: &Db, _project: &Project, _from: u64) -> Result<()> { Ok(()) } /// Fetches one version of main older than this machine keeps. fn fetch_version(&self, _db: &Db, _project: &Project, _n: u64) -> Result<()> { Ok(()) } /// Records a change's state with the host so other machines and people /// can see it (no-op for local projects). fn publish_change(&self, _db: &Db, _store: &Store, _project: &Project, _change: &Change, _workspace: &str) -> Result<()> { Ok(()) } } /// A project that lives only on this machine. Landing is a merge plus one /// row in the versions table, decided under SQLite's write lock. pub struct Local; impl Host for Local { fn refresh(&self, db: &Db, _store: &Store, project: &Project) -> Result> { db.latest_version(&project.id) } fn fetch_tree(&self, _store: &Store, _project: &Project, _tree: &Id, _have: Option<&Id>) -> Result<()> { Ok(()) } fn land(&self, db: &Db, store: &Store, project: &Project, request: &LandRequest) -> Result { self.land_on(db, store, &project.id, request) } } impl Local { /// Lands under SQLite's write lock, so landings on one main never race. pub fn land_on(&self, db: &Db, store: &Store, project: &str, request: &LandRequest) -> Result { db.conn.execute_batch("BEGIN IMMEDIATE")?; let result = land_in(db, store, project, request); match &result { Ok(_) => db.conn.execute_batch("COMMIT")?, Err(_) => { let _ = db.conn.execute_batch("ROLLBACK"); } } result } } /// The landing itself, for local projects and the self-hosted server: merge /// the change onto the latest main and add the next version. pub fn land_in(db: &Db, store: &Store, project: &str, request: &LandRequest) -> Result { let main = db.latest_version(project)?; let main_tree = main.as_ref().map(|v| v.tree).unwrap_or_else(tree::empty_id); let tree = if main_tree == request.base_tree { request.tree } else { let m = merge::trees( store, Some(&request.base_tree), &main_tree, &request.tree, Mode::Land, (&format!("main {}", main.as_ref().map(|v| v.n).unwrap_or(0)), "change"), )?; if !m.clean() { return Ok(Landed::Conflict { main: main.as_ref().map(|v| v.n).unwrap_or(0), conflicts: m.conflicts }); } // Keep the objects the merge made. for o in m.objects.objects.values() { store.put(o)?; } m.tree }; let version = Version { n: main.as_ref().map(|v| v.n + 1).unwrap_or(1), tree, prev: main.as_ref().map(|v| v.id(project)), change: request.change.clone(), author: request.author.clone(), time: ids::now(), title: request.title.clone(), body: request.body.clone(), git: None, }; db.put_version(project, &version)?; Ok(Landed::Landed { version }) }