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 | /// The change itself, recorded first (a hosted project's own change), so |
| 28 | /// landing is one call. |
| 29 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 30 | pub record: Option<crate::wire::PutChange>, |
| 31 | /// When it should land (seconds since the epoch); the host keeps it until then. |
| 32 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 33 | pub at: Option<i64>, |
| 34 | /// The lander's signature over the change (vcs key). |
| 35 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 36 | pub signature: Option<LandSignature>, |
| 37 | } |
| 38 |
| 39 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 40 | pub struct LandSignature { |
| 41 | pub key: String, |
| 42 | pub sig: String, |
| 43 | } |
| 44 |
| 45 | impl LandSignature { |
| 46 | /// This machine's signature for a land on a hosted project, if it has a key. |
| 47 | pub fn for_land(address: &str, change: &str, base: &Id, tree: &Id, title: &str, body: &str, author: &str) -> Option<LandSignature> { |
| 48 | crate::keys::sign(address, change, base, tree, title, body, author).map(|(key, sig)| LandSignature { key, sig }) |
| 49 | } |
| 50 | } |
| 51 |
| 52 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 53 | #[serde(tag = "outcome", rename_all = "snake_case")] |
| 54 | pub enum Landed { |
| 55 | Landed { version: Version }, |
| 56 | Conflict { main: u64, conflicts: Vec<Conflict> }, |
| 57 | Waiting { asked: Vec<String>, reason: String }, |
| 58 | ChecksFailed { check: String, log: Option<String> }, |
| 59 | /// The repository's rules send it through review: a change request on |
| 60 | /// the host that lands once its approvals and checks pass. |
| 61 | Review { |
| 62 | number: u64, |
| 63 | url: String, |
| 64 | #[serde(default)] |
| 65 | blockers: Vec<String>, |
| 66 | /// It can't merge before this time (land --at). |
| 67 | #[serde(default)] |
| 68 | at: Option<i64>, |
| 69 | }, |
| 70 | /// The host lands it at `at` (land --at), as it is now. |
| 71 | Scheduled { at: i64 }, |
| 72 | } |
| 73 |
| 74 | pub trait Host { |
| 75 | /// Brings the local copy of main up to date and returns its latest |
| 76 | /// version (None: the project has no versions yet). |
| 77 | fn refresh(&self, db: &Db, store: &Store, project: &Project) -> Result<Option<Version>>; |
| 78 |
| 79 | /// Makes sure every object under `tree` is in the store. `have` names a |
| 80 | /// tree whose objects are already here, to skip what they share. |
| 81 | fn fetch_tree(&self, store: &Store, project: &Project, tree: &Id, have: Option<&Id>) -> Result<()>; |
| 82 |
| 83 | fn land(&self, db: &Db, store: &Store, project: &Project, request: &LandRequest) -> Result<Landed>; |
| 84 |
| 85 | /// Fetches main's versions from `from` up to the first one this |
| 86 | /// machine keeps (local projects keep them all). |
| 87 | fn fetch_history(&self, _db: &Db, _project: &Project, _from: u64) -> Result<()> { |
| 88 | Ok(()) |
| 89 | } |
| 90 |
| 91 | /// Fetches one version of main older than this machine keeps. |
| 92 | fn fetch_version(&self, _db: &Db, _project: &Project, _n: u64) -> Result<()> { |
| 93 | Ok(()) |
| 94 | } |
| 95 |
| 96 | /// Records a change's state with the host so other machines and people |
| 97 | /// can see it (no-op for local projects). |
| 98 | fn publish_change(&self, _db: &Db, _store: &Store, _project: &Project, _change: &Change, _workspace: &str) -> Result<()> { |
| 99 | Ok(()) |
| 100 | } |
| 101 | } |
| 102 |
| 103 | /// A project that lives only on this machine. Landing is a merge plus one |
| 104 | /// row in the versions table, decided under SQLite's write lock. |
| 105 | pub struct Local; |
| 106 |
| 107 | impl Host for Local { |
| 108 | fn refresh(&self, db: &Db, _store: &Store, project: &Project) -> Result<Option<Version>> { |
| 109 | db.latest_version(&project.id) |
| 110 | } |
| 111 |
| 112 | fn fetch_tree(&self, _store: &Store, _project: &Project, _tree: &Id, _have: Option<&Id>) -> Result<()> { |
| 113 | Ok(()) |
| 114 | } |
| 115 |
| 116 | fn land(&self, db: &Db, store: &Store, project: &Project, request: &LandRequest) -> Result<Landed> { |
| 117 | self.land_on(db, store, &project.id, request) |
| 118 | } |
| 119 | } |
| 120 |
| 121 | impl Local { |
| 122 | /// Lands under SQLite's write lock, so landings on one main never race. |
| 123 | pub fn land_on(&self, db: &Db, store: &Store, project: &str, request: &LandRequest) -> Result<Landed> { |
| 124 | db.conn.execute_batch("BEGIN IMMEDIATE")?; |
| 125 | let result = land_in(db, store, project, request); |
| 126 | match &result { |
| 127 | Ok(_) => db.conn.execute_batch("COMMIT")?, |
| 128 | Err(_) => { |
| 129 | let _ = db.conn.execute_batch("ROLLBACK"); |
| 130 | } |
| 131 | } |
| 132 | result |
| 133 | } |
| 134 | } |
| 135 |
| 136 | /// The landing itself, for local projects and the self-hosted server: merge |
| 137 | /// the change onto the latest main and add the next version. |
| 138 | pub fn land_in(db: &Db, store: &Store, project: &str, request: &LandRequest) -> Result<Landed> { |
| 139 | let main = db.latest_version(project)?; |
| 140 | let main_tree = main.as_ref().map(|v| v.tree).unwrap_or_else(tree::empty_id); |
| 141 | let tree = if main_tree == request.base_tree { |
| 142 | request.tree |
| 143 | } else { |
| 144 | let m = merge::trees( |
| 145 | store, |
| 146 | Some(&request.base_tree), |
| 147 | &main_tree, |
| 148 | &request.tree, |
| 149 | Mode::Land, |
| 150 | (&format!("main {}", main.as_ref().map(|v| v.n).unwrap_or(0)), "change"), |
| 151 | )?; |
| 152 | if !m.clean() { |
| 153 | return Ok(Landed::Conflict { main: main.as_ref().map(|v| v.n).unwrap_or(0), conflicts: m.conflicts }); |
| 154 | } |
| 155 | // Keep the objects the merge made. |
| 156 | for o in m.objects.objects.values() { |
| 157 | store.put(o)?; |
| 158 | } |
| 159 | m.tree |
| 160 | }; |
| 161 | let version = Version { |
| 162 | n: main.as_ref().map(|v| v.n + 1).unwrap_or(1), |
| 163 | tree, |
| 164 | prev: main.as_ref().map(|v| v.id(project)), |
| 165 | change: request.change.clone(), |
| 166 | author: request.author.clone(), |
| 167 | time: ids::now(), |
| 168 | title: request.title.clone(), |
| 169 | body: request.body.clone(), |
| 170 | git: None, |
| 171 | }; |
| 172 | db.put_version(project, &version)?; |
| 173 | Ok(Landed::Landed { version }) |
| 174 | } |
| 175 |