Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 | use serde::{Deserialize, Serialize}; |
| 7 | use vcs_core::merge::{self, Conflict, Mode}; |
| 8 | use vcs_core::version::Version; |
| 9 | use vcs_core::{Id, tree}; |
| 10 |
| 11 | use crate::db::{Change, Db, Project}; |
| 12 | use crate::ids; |
| 13 | use crate::objects::Store; |
| 14 |
| 15 | /// What landing a change asks for. |
| 16 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 17 | pub struct LandRequest { |
| 18 | pub change: String, |
| 19 | pub base_tree: Id, |
| 20 | pub tree: Id, |
| 21 | pub title: String, |
| 22 | pub body: String, |
| 23 | pub author: String, |
| 24 | /// Allow something the secret check flagged (recorded). |
| 25 | #[serde(default)] |
| 26 | pub allow_secrets: bool, |
| 27 | } |
| 28 |
| 29 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 30 | #[serde(tag = "outcome", rename_all = "snake_case")] |
| 31 | pub enum Landed { |
| 32 | Landed { version: Version }, |
| 33 | Conflict { main: u64, conflicts: Vec<Conflict> }, |
| 34 | Waiting { asked: Vec<String>, reason: String }, |
| 35 | ChecksFailed { check: String, log: Option<String> }, |
| 36 | } |
| 37 |
| 38 | pub trait Host { |
| 39 | /// Brings the local copy of main up to date and returns its latest |
| 40 | /// version (None: the project has no versions yet). |
| 41 | fn refresh(&self, db: &Db, store: &Store, project: &Project) -> Result<Option<Version>>; |
| 42 |
| 43 | /// Makes sure every object under `tree` is in the store. `have` names a |
| 44 | /// tree whose objects are already here, to skip what they share. |
| 45 | fn fetch_tree(&self, store: &Store, project: &Project, tree: &Id, have: Option<&Id>) -> Result<()>; |
| 46 |
| 47 | fn land(&self, db: &Db, store: &Store, project: &Project, request: &LandRequest) -> Result<Landed>; |
| 48 |
| 49 | /// Records a change's state with the host so other machines and people |
| 50 | /// can see it (no-op for local projects). |
| 51 | fn publish_change(&self, _db: &Db, _store: &Store, _project: &Project, _change: &Change, _workspace: &str) -> Result<()> { |
| 52 | Ok(()) |
| 53 | } |
| 54 | } |
| 55 |
| 56 | /// A project that lives only on this machine. Landing is a merge plus one |
| 57 | /// row in the versions table, decided under SQLite's write lock. |
| 58 | pub struct Local; |
| 59 |
| 60 | impl Host for Local { |
| 61 | fn refresh(&self, db: &Db, _store: &Store, project: &Project) -> Result<Option<Version>> { |
| 62 | db.latest_version(&project.id) |
| 63 | } |
| 64 |
| 65 | fn fetch_tree(&self, _store: &Store, _project: &Project, _tree: &Id, _have: Option<&Id>) -> Result<()> { |
| 66 | Ok(()) |
| 67 | } |
| 68 |
| 69 | fn land(&self, db: &Db, store: &Store, project: &Project, request: &LandRequest) -> Result<Landed> { |
| 70 | self.land_on(db, store, &project.id, request) |
| 71 | } |
| 72 | } |
| 73 |
| 74 | impl Local { |
| 75 | /// Lands under SQLite's write lock, so landings on one main never race. |
| 76 | pub fn land_on(&self, db: &Db, store: &Store, project: &str, request: &LandRequest) -> Result<Landed> { |
| 77 | db.conn.execute_batch("BEGIN IMMEDIATE")?; |
| 78 | let result = land_in(db, store, project, request); |
| 79 | match &result { |
| 80 | Ok(_) => db.conn.execute_batch("COMMIT")?, |
| 81 | Err(_) => { |
| 82 | let _ = db.conn.execute_batch("ROLLBACK"); |
| 83 | } |
| 84 | } |
| 85 | result |
| 86 | } |
| 87 | } |
| 88 |
| 89 | /// The landing itself, for local projects and the self-hosted server: merge |
| 90 | /// the change onto the latest main and add the next version. |
| 91 | pub fn land_in(db: &Db, store: &Store, project: &str, request: &LandRequest) -> Result<Landed> { |
| 92 | let main = db.latest_version(project)?; |
| 93 | let main_tree = main.as_ref().map(|v| v.tree).unwrap_or_else(tree::empty_id); |
| 94 | let tree = if main_tree == request.base_tree { |
| 95 | request.tree |
| 96 | } else { |
| 97 | let m = merge::trees( |
| 98 | store, |
| 99 | Some(&request.base_tree), |
| 100 | &main_tree, |
| 101 | &request.tree, |
| 102 | Mode::Land, |
| 103 | (&format!("main {}", main.as_ref().map(|v| v.n).unwrap_or(0)), "change"), |
| 104 | )?; |
| 105 | if !m.clean() { |
| 106 | return Ok(Landed::Conflict { main: main.as_ref().map(|v| v.n).unwrap_or(0), conflicts: m.conflicts }); |
| 107 | } |
| 108 | // Keep the objects the merge made. |
| 109 | for o in m.objects.objects.values() { |
| 110 | store.put(o)?; |
| 111 | } |
| 112 | m.tree |
| 113 | }; |
| 114 | let version = Version { |
| 115 | n: main.as_ref().map(|v| v.n + 1).unwrap_or(1), |
| 116 | tree, |
| 117 | prev: main.as_ref().map(|v| v.id(project)), |
| 118 | change: request.change.clone(), |
| 119 | author: request.author.clone(), |
| 120 | time: ids::now(), |
| 121 | title: request.title.clone(), |
| 122 | body: request.body.clone(), |
| 123 | git: None, |
| 124 | }; |
| 125 | db.put_version(project, &version)?; |
| 126 | Ok(Landed::Landed { version }) |
| 127 | } |
| 128 |