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