Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 | use std::path::{Path, PathBuf}; |
| 7 |
| 8 | use anyhow::{Context, Result}; |
| 9 | use rusqlite::{Connection, OptionalExtension, params}; |
| 10 | use serde::{Deserialize, Serialize}; |
| 11 | use vcs_core::Id; |
| 12 | use vcs_core::version::Version; |
| 13 |
| 14 | use crate::ids; |
| 15 |
| 16 | const SCHEMA: &str = r#" |
| 17 | CREATE TABLE IF NOT EXISTS projects ( |
| 18 | id TEXT PRIMARY KEY, |
| 19 | address TEXT NOT NULL UNIQUE, |
| 20 | kind TEXT NOT NULL, |
| 21 | api TEXT, |
| 22 | checked INTEGER NOT NULL DEFAULT 0, |
| 23 | created INTEGER NOT NULL |
| 24 | ); |
| 25 | CREATE TABLE IF NOT EXISTS versions ( |
| 26 | project TEXT NOT NULL, |
| 27 | n INTEGER NOT NULL, |
| 28 | id TEXT NOT NULL, |
| 29 | tree TEXT NOT NULL, |
| 30 | prev TEXT, |
| 31 | change TEXT NOT NULL, |
| 32 | author TEXT NOT NULL, |
| 33 | time INTEGER NOT NULL, |
| 34 | title TEXT NOT NULL, |
| 35 | body TEXT NOT NULL, |
| 36 | git TEXT, |
| 37 | PRIMARY KEY (project, n) |
| 38 | ); |
| 39 | CREATE INDEX IF NOT EXISTS versions_change ON versions(project, change); |
| 40 | CREATE TABLE IF NOT EXISTS workspaces ( |
| 41 | id TEXT PRIMARY KEY, |
| 42 | project TEXT NOT NULL, |
| 43 | path TEXT NOT NULL UNIQUE, |
| 44 | change TEXT NOT NULL, |
| 45 | only TEXT NOT NULL DEFAULT '[]', |
| 46 | created INTEGER NOT NULL, |
| 47 | used INTEGER NOT NULL |
| 48 | ); |
| 49 | CREATE TABLE IF NOT EXISTS changes ( |
| 50 | id TEXT PRIMARY KEY, |
| 51 | project TEXT NOT NULL, |
| 52 | title TEXT NOT NULL DEFAULT '', |
| 53 | body TEXT NOT NULL DEFAULT '', |
| 54 | base_tree TEXT NOT NULL, |
| 55 | base_version INTEGER, |
| 56 | on_change TEXT, |
| 57 | after_change TEXT, |
| 58 | head_tree TEXT, |
| 59 | saves INTEGER NOT NULL DEFAULT 0, |
| 60 | state TEXT NOT NULL DEFAULT 'open', |
| 61 | version INTEGER, |
| 62 | author TEXT NOT NULL DEFAULT '', |
| 63 | conflicts TEXT NOT NULL DEFAULT '[]', |
| 64 | workspace TEXT, |
| 65 | shared TEXT NOT NULL DEFAULT '[]', |
| 66 | remote_rev INTEGER NOT NULL DEFAULT 0, |
| 67 | created INTEGER NOT NULL, |
| 68 | updated INTEGER NOT NULL |
| 69 | ); |
| 70 | CREATE INDEX IF NOT EXISTS changes_project ON changes(project, state); |
| 71 | CREATE TABLE IF NOT EXISTS saves ( |
| 72 | change TEXT NOT NULL, |
| 73 | n INTEGER NOT NULL, |
| 74 | tree TEXT NOT NULL, |
| 75 | base_tree TEXT NOT NULL, |
| 76 | at INTEGER NOT NULL, |
| 77 | workspace TEXT, |
| 78 | uploaded INTEGER NOT NULL DEFAULT 0, |
| 79 | PRIMARY KEY (change, n) |
| 80 | ); |
| 81 | CREATE INDEX IF NOT EXISTS saves_time ON saves(workspace, at); |
| 82 | CREATE TABLE IF NOT EXISTS ops ( |
| 83 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 84 | workspace TEXT NOT NULL, |
| 85 | at INTEGER NOT NULL, |
| 86 | kind TEXT NOT NULL, |
| 87 | data TEXT NOT NULL, |
| 88 | undone INTEGER NOT NULL DEFAULT 0 |
| 89 | ); |
| 90 | CREATE INDEX IF NOT EXISTS ops_workspace ON ops(workspace, id); |
| 91 | CREATE TABLE IF NOT EXISTS complete ( |
| 92 | tree TEXT PRIMARY KEY |
| 93 | ); |
| 94 | CREATE TABLE IF NOT EXISTS personal ( |
| 95 | project TEXT PRIMARY KEY, |
| 96 | patterns TEXT NOT NULL |
| 97 | ); |
| 98 | CREATE TABLE IF NOT EXISTS mine ( |
| 99 | project TEXT NOT NULL, |
| 100 | path TEXT NOT NULL, |
| 101 | file TEXT NOT NULL, |
| 102 | exec INTEGER NOT NULL DEFAULT 0, |
| 103 | at INTEGER NOT NULL, |
| 104 | PRIMARY KEY (project, path) |
| 105 | ); |
| 106 | "#; |
| 107 |
| 108 | pub struct Db { |
| 109 | pub conn: Connection, |
| 110 | } |
| 111 |
| 112 | #[derive(Debug, Clone)] |
| 113 | pub struct Project { |
| 114 | pub id: String, |
| 115 | /// `host/owner/name` for hosted projects, the folder for local ones. |
| 116 | pub address: String, |
| 117 | /// "local" or "hosted". |
| 118 | pub kind: String, |
| 119 | pub api: Option<String>, |
| 120 | /// When main was last checked with the host (seconds). |
| 121 | pub checked: i64, |
| 122 | } |
| 123 |
| 124 | impl Project { |
| 125 | pub fn hosted(&self) -> bool { |
| 126 | self.kind == "hosted" |
| 127 | } |
| 128 | } |
| 129 |
| 130 | #[derive(Debug, Clone)] |
| 131 | pub struct Workspace { |
| 132 | pub id: String, |
| 133 | pub project: String, |
| 134 | pub path: PathBuf, |
| 135 | pub change: String, |
| 136 | pub only: Vec<String>, |
| 137 | } |
| 138 |
| 139 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 140 | pub struct Conflict { |
| 141 | pub path: String, |
| 142 | #[serde(default)] |
| 143 | pub version: Option<u64>, |
| 144 | } |
| 145 |
| 146 | #[derive(Debug, Clone)] |
| 147 | pub struct Change { |
| 148 | pub id: String, |
| 149 | pub project: String, |
| 150 | pub title: String, |
| 151 | pub body: String, |
| 152 | /// The tree the change starts from. |
| 153 | pub base_tree: Id, |
| 154 | /// Set when the base is a version of main. |
| 155 | pub base_version: Option<u64>, |
| 156 | /// Set when stacked on an unlanded change. |
| 157 | pub on_change: Option<String>, |
| 158 | /// Set when the change continues after one that landed from the same workspace. |
| 159 | pub after_change: Option<String>, |
| 160 | /// The latest save (None: nothing saved, same as the base). |
| 161 | pub head_tree: Option<Id>, |
| 162 | pub saves: i64, |
| 163 | /// open, waiting, landed, dropped. |
| 164 | pub state: String, |
| 165 | pub version: Option<u64>, |
| 166 | pub author: String, |
| 167 | pub conflicts: Vec<Conflict>, |
| 168 | pub workspace: Option<String>, |
| 169 | pub shared: Vec<String>, |
| 170 | pub remote_rev: i64, |
| 171 | pub created: i64, |
| 172 | pub updated: i64, |
| 173 | } |
| 174 |
| 175 | impl Change { |
| 176 | pub fn new(id: String, project: &str, base_tree: Id, base_version: Option<u64>, author: &str) -> Change { |
| 177 | let now = ids::now(); |
| 178 | Change { |
| 179 | id, |
| 180 | project: project.to_string(), |
| 181 | title: String::new(), |
| 182 | body: String::new(), |
| 183 | base_tree, |
| 184 | base_version, |
| 185 | on_change: None, |
| 186 | after_change: None, |
| 187 | head_tree: None, |
| 188 | saves: 0, |
| 189 | state: "open".into(), |
| 190 | version: None, |
| 191 | author: author.to_string(), |
| 192 | conflicts: Vec::new(), |
| 193 | workspace: None, |
| 194 | shared: Vec::new(), |
| 195 | remote_rev: 0, |
| 196 | created: now, |
| 197 | updated: now, |
| 198 | } |
| 199 | } |
| 200 |
| 201 | /// The change's content: its latest save, or its base. |
| 202 | pub fn tree(&self) -> Id { |
| 203 | self.head_tree.unwrap_or(self.base_tree) |
| 204 | } |
| 205 |
| 206 | pub fn is_empty(&self) -> bool { |
| 207 | self.tree() == self.base_tree |
| 208 | } |
| 209 |
| 210 | pub fn message(&self) -> String { |
| 211 | if self.body.is_empty() { self.title.clone() } else { format!("{}\n\n{}", self.title, self.body) } |
| 212 | } |
| 213 | } |
| 214 |
| 215 | #[derive(Debug, Clone)] |
| 216 | pub struct Save { |
| 217 | pub change: String, |
| 218 | pub n: i64, |
| 219 | pub tree: Id, |
| 220 | pub base_tree: Id, |
| 221 | pub at: i64, |
| 222 | pub uploaded: bool, |
| 223 | } |
| 224 |
| 225 | #[derive(Debug, Clone)] |
| 226 | pub struct Op { |
| 227 | pub id: i64, |
| 228 | pub at: i64, |
| 229 | pub kind: String, |
| 230 | pub data: serde_json::Value, |
| 231 | } |
| 232 |
| 233 | fn id_col(row: &rusqlite::Row, i: usize) -> rusqlite::Result<Id> { |
| 234 | let text: String = row.get(i)?; |
| 235 | Id::parse(&text).ok_or_else(|| rusqlite::Error::InvalidColumnType(i, "id".into(), rusqlite::types::Type::Text)) |
| 236 | } |
| 237 |
| 238 | fn opt_id_col(row: &rusqlite::Row, i: usize) -> rusqlite::Result<Option<Id>> { |
| 239 | let text: Option<String> = row.get(i)?; |
| 240 | Ok(text.and_then(|t| Id::parse(&t))) |
| 241 | } |
| 242 |
| 243 | impl Db { |
| 244 | pub fn open(home: &Path) -> Result<Db> { |
| 245 | std::fs::create_dir_all(home).with_context(|| format!("creating {}", home.display()))?; |
| 246 | let conn = Connection::open(home.join("vcs.db")).context("opening the vcs database")?; |
| 247 | Self::setup(conn) |
| 248 | } |
| 249 |
| 250 | pub fn setup(conn: Connection) -> Result<Db> { |
| 251 | conn.busy_timeout(std::time::Duration::from_secs(60))?; |
| 252 | conn.pragma_update(None, "journal_mode", "WAL")?; |
| 253 | conn.pragma_update(None, "synchronous", "NORMAL")?; |
| 254 | conn.execute_batch(SCHEMA)?; |
| 255 | Ok(Db { conn }) |
| 256 | } |
| 257 |
| 258 | // Projects. |
| 259 |
| 260 | pub fn put_project(&self, p: &Project) -> Result<()> { |
| 261 | self.conn.execute( |
| 262 | "INSERT INTO projects (id, address, kind, api, checked, created) VALUES (?1, ?2, ?3, ?4, ?5, ?6) |
| 263 | ON CONFLICT(id) DO UPDATE SET address=excluded.address, kind=excluded.kind, api=excluded.api, checked=excluded.checked", |
| 264 | params![p.id, p.address, p.kind, p.api, p.checked, ids::now()], |
| 265 | )?; |
| 266 | Ok(()) |
| 267 | } |
| 268 |
| 269 | fn project_row(row: &rusqlite::Row) -> rusqlite::Result<Project> { |
| 270 | Ok(Project { id: row.get(0)?, address: row.get(1)?, kind: row.get(2)?, api: row.get(3)?, checked: row.get(4)? }) |
| 271 | } |
| 272 |
| 273 | pub fn project(&self, id: &str) -> Result<Option<Project>> { |
| 274 | Ok(self |
| 275 | .conn |
| 276 | .query_row("SELECT id, address, kind, api, checked FROM projects WHERE id = ?1", [id], Self::project_row) |
| 277 | .optional()?) |
| 278 | } |
| 279 |
| 280 | pub fn project_by_address(&self, address: &str) -> Result<Option<Project>> { |
| 281 | Ok(self |
| 282 | .conn |
| 283 | .query_row("SELECT id, address, kind, api, checked FROM projects WHERE address = ?1", [address], Self::project_row) |
| 284 | .optional()?) |
| 285 | } |
| 286 |
| 287 | pub fn set_checked(&self, project: &str, at: i64) -> Result<()> { |
| 288 | self.conn.execute("UPDATE projects SET checked = ?2 WHERE id = ?1", params![project, at])?; |
| 289 | Ok(()) |
| 290 | } |
| 291 |
| 292 | // Versions. |
| 293 |
| 294 | pub fn put_version(&self, project: &str, v: &Version) -> Result<()> { |
| 295 | self.conn.execute( |
| 296 | "INSERT OR REPLACE INTO versions (project, n, id, tree, prev, change, author, time, title, body, git) |
| 297 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", |
| 298 | params![ |
| 299 | project, |
| 300 | v.n as i64, |
| 301 | v.id(project).hex(), |
| 302 | v.tree.hex(), |
| 303 | v.prev.map(|p| p.hex()), |
| 304 | v.change, |
| 305 | v.author, |
| 306 | v.time, |
| 307 | v.title, |
| 308 | v.body, |
| 309 | v.git |
| 310 | ], |
| 311 | )?; |
| 312 | Ok(()) |
| 313 | } |
| 314 |
| 315 | fn version_row(row: &rusqlite::Row) -> rusqlite::Result<Version> { |
| 316 | Ok(Version { |
| 317 | n: row.get::<_, i64>(0)? as u64, |
| 318 | tree: id_col(row, 1)?, |
| 319 | prev: opt_id_col(row, 2)?, |
| 320 | change: row.get(3)?, |
| 321 | author: row.get(4)?, |
| 322 | time: row.get(5)?, |
| 323 | title: row.get(6)?, |
| 324 | body: row.get(7)?, |
| 325 | git: row.get(8)?, |
| 326 | }) |
| 327 | } |
| 328 |
| 329 | const VERSION_COLUMNS: &'static str = "n, tree, prev, change, author, time, title, body, git"; |
| 330 |
| 331 | pub fn version(&self, project: &str, n: u64) -> Result<Option<Version>> { |
| 332 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 AND n = ?2", Self::VERSION_COLUMNS); |
| 333 | Ok(self.conn.query_row(&sql, params![project, n as i64], Self::version_row).optional()?) |
| 334 | } |
| 335 |
| 336 | pub fn latest_version(&self, project: &str) -> Result<Option<Version>> { |
| 337 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 ORDER BY n DESC LIMIT 1", Self::VERSION_COLUMNS); |
| 338 | Ok(self.conn.query_row(&sql, [project], Self::version_row).optional()?) |
| 339 | } |
| 340 |
| 341 | pub fn versions(&self, project: &str, limit: i64) -> Result<Vec<Version>> { |
| 342 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 ORDER BY n DESC LIMIT ?2", Self::VERSION_COLUMNS); |
| 343 | let mut stmt = self.conn.prepare(&sql)?; |
| 344 | let rows = stmt.query_map(params![project, limit], Self::version_row)?; |
| 345 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 346 | } |
| 347 |
| 348 | pub fn version_of_change(&self, project: &str, change: &str) -> Result<Option<Version>> { |
| 349 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 AND change = ?2 ORDER BY n DESC LIMIT 1", Self::VERSION_COLUMNS); |
| 350 | Ok(self.conn.query_row(&sql, params![project, change], Self::version_row).optional()?) |
| 351 | } |
| 352 |
| 353 | // Workspaces. |
| 354 |
| 355 | pub fn put_workspace(&self, w: &Workspace) -> Result<()> { |
| 356 | let now = ids::now(); |
| 357 | self.conn.execute( |
| 358 | "INSERT INTO workspaces (id, project, path, change, only, created, used) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6) |
| 359 | ON CONFLICT(id) DO UPDATE SET project=excluded.project, path=excluded.path, change=excluded.change, |
| 360 | only=excluded.only, used=excluded.used", |
| 361 | params![w.id, w.project, w.path.to_string_lossy(), w.change, serde_json::to_string(&w.only)?, now], |
| 362 | )?; |
| 363 | Ok(()) |
| 364 | } |
| 365 |
| 366 | fn workspace_row(row: &rusqlite::Row) -> rusqlite::Result<Workspace> { |
| 367 | let only: String = row.get(4)?; |
| 368 | Ok(Workspace { |
| 369 | id: row.get(0)?, |
| 370 | project: row.get(1)?, |
| 371 | path: PathBuf::from(row.get::<_, String>(2)?), |
| 372 | change: row.get(3)?, |
| 373 | only: serde_json::from_str(&only).unwrap_or_default(), |
| 374 | }) |
| 375 | } |
| 376 |
| 377 | pub fn workspace(&self, id: &str) -> Result<Option<Workspace>> { |
| 378 | Ok(self |
| 379 | .conn |
| 380 | .query_row("SELECT id, project, path, change, only FROM workspaces WHERE id = ?1", [id], Self::workspace_row) |
| 381 | .optional()?) |
| 382 | } |
| 383 |
| 384 | pub fn project_workspaces(&self, project: &str) -> Result<Vec<Workspace>> { |
| 385 | let mut stmt = |
| 386 | self.conn.prepare("SELECT id, project, path, change, only FROM workspaces WHERE project = ?1 ORDER BY used DESC")?; |
| 387 | let rows = stmt.query_map([project], Self::workspace_row)?; |
| 388 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 389 | } |
| 390 |
| 391 | pub fn all_workspaces(&self) -> Result<Vec<Workspace>> { |
| 392 | let mut stmt = self.conn.prepare("SELECT id, project, path, change, only FROM workspaces ORDER BY used DESC")?; |
| 393 | let rows = stmt.query_map([], Self::workspace_row)?; |
| 394 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 395 | } |
| 396 |
| 397 | pub fn workspace_holding(&self, change: &str) -> Result<Option<Workspace>> { |
| 398 | Ok(self |
| 399 | .conn |
| 400 | .query_row( |
| 401 | "SELECT id, project, path, change, only FROM workspaces WHERE change = ?1 ORDER BY used DESC LIMIT 1", |
| 402 | [change], |
| 403 | Self::workspace_row, |
| 404 | ) |
| 405 | .optional()?) |
| 406 | } |
| 407 |
| 408 | pub fn delete_workspace(&self, id: &str) -> Result<()> { |
| 409 | self.conn.execute("DELETE FROM workspaces WHERE id = ?1", [id])?; |
| 410 | Ok(()) |
| 411 | } |
| 412 |
| 413 | // Changes. |
| 414 |
| 415 | pub fn put_change(&self, c: &Change) -> Result<()> { |
| 416 | self.conn.execute( |
| 417 | "INSERT INTO changes (id, project, title, body, base_tree, base_version, on_change, after_change, head_tree, |
| 418 | saves, state, version, author, conflicts, workspace, shared, remote_rev, created, updated) |
| 419 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19) |
| 420 | ON CONFLICT(id) DO UPDATE SET title=excluded.title, body=excluded.body, base_tree=excluded.base_tree, |
| 421 | base_version=excluded.base_version, on_change=excluded.on_change, after_change=excluded.after_change, |
| 422 | head_tree=excluded.head_tree, saves=excluded.saves, state=excluded.state, version=excluded.version, |
| 423 | author=excluded.author, conflicts=excluded.conflicts, workspace=excluded.workspace, |
| 424 | shared=excluded.shared, remote_rev=excluded.remote_rev, updated=excluded.updated", |
| 425 | params![ |
| 426 | c.id, |
| 427 | c.project, |
| 428 | c.title, |
| 429 | c.body, |
| 430 | c.base_tree.hex(), |
| 431 | c.base_version.map(|v| v as i64), |
| 432 | c.on_change, |
| 433 | c.after_change, |
| 434 | c.head_tree.map(|t| t.hex()), |
| 435 | c.saves, |
| 436 | c.state, |
| 437 | c.version.map(|v| v as i64), |
| 438 | c.author, |
| 439 | serde_json::to_string(&c.conflicts)?, |
| 440 | c.workspace, |
| 441 | serde_json::to_string(&c.shared)?, |
| 442 | c.remote_rev, |
| 443 | c.created, |
| 444 | ids::now() |
| 445 | ], |
| 446 | )?; |
| 447 | Ok(()) |
| 448 | } |
| 449 |
| 450 | const CHANGE_COLUMNS: &'static str = "id, project, title, body, base_tree, base_version, on_change, after_change, head_tree, saves, state, version, author, conflicts, workspace, shared, remote_rev, created, updated"; |
| 451 |
| 452 | fn change_row(row: &rusqlite::Row) -> rusqlite::Result<Change> { |
| 453 | let conflicts: String = row.get(13)?; |
| 454 | let shared: String = row.get(15)?; |
| 455 | Ok(Change { |
| 456 | id: row.get(0)?, |
| 457 | project: row.get(1)?, |
| 458 | title: row.get(2)?, |
| 459 | body: row.get(3)?, |
| 460 | base_tree: id_col(row, 4)?, |
| 461 | base_version: row.get::<_, Option<i64>>(5)?.map(|v| v as u64), |
| 462 | on_change: row.get(6)?, |
| 463 | after_change: row.get(7)?, |
| 464 | head_tree: opt_id_col(row, 8)?, |
| 465 | saves: row.get(9)?, |
| 466 | state: row.get(10)?, |
| 467 | version: row.get::<_, Option<i64>>(11)?.map(|v| v as u64), |
| 468 | author: row.get(12)?, |
| 469 | conflicts: serde_json::from_str(&conflicts).unwrap_or_default(), |
| 470 | workspace: row.get(14)?, |
| 471 | shared: serde_json::from_str(&shared).unwrap_or_default(), |
| 472 | remote_rev: row.get(16)?, |
| 473 | created: row.get(17)?, |
| 474 | updated: row.get(18)?, |
| 475 | }) |
| 476 | } |
| 477 |
| 478 | pub fn change(&self, id: &str) -> Result<Option<Change>> { |
| 479 | let sql = format!("SELECT {} FROM changes WHERE id = ?1", Self::CHANGE_COLUMNS); |
| 480 | Ok(self.conn.query_row(&sql, [id], Self::change_row).optional()?) |
| 481 | } |
| 482 |
| 483 | pub fn changes(&self, project: &str) -> Result<Vec<Change>> { |
| 484 | let sql = format!("SELECT {} FROM changes WHERE project = ?1 ORDER BY updated DESC", Self::CHANGE_COLUMNS); |
| 485 | let mut stmt = self.conn.prepare(&sql)?; |
| 486 | let rows = stmt.query_map([project], Self::change_row)?; |
| 487 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 488 | } |
| 489 |
| 490 | pub fn change_ids(&self, project: &str) -> Result<Vec<String>> { |
| 491 | let mut stmt = self.conn.prepare("SELECT id FROM changes WHERE project = ?1")?; |
| 492 | let rows = stmt.query_map([project], |r| r.get::<_, String>(0))?; |
| 493 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 494 | } |
| 495 |
| 496 | pub fn changes_by_prefix(&self, project: &str, prefix: &str) -> Result<Vec<Change>> { |
| 497 | let sql = format!("SELECT {} FROM changes WHERE project = ?1 AND id LIKE ?2 || '%'", Self::CHANGE_COLUMNS); |
| 498 | let mut stmt = self.conn.prepare(&sql)?; |
| 499 | let rows = stmt.query_map(params![project, prefix], Self::change_row)?; |
| 500 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 501 | } |
| 502 |
| 503 | // Saves. |
| 504 |
| 505 | pub fn add_save(&self, s: &Save, workspace: &str) -> Result<()> { |
| 506 | self.conn.execute( |
| 507 | "INSERT OR REPLACE INTO saves (change, n, tree, base_tree, at, workspace, uploaded) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", |
| 508 | params![s.change, s.n, s.tree.hex(), s.base_tree.hex(), s.at, workspace, s.uploaded as i64], |
| 509 | )?; |
| 510 | Ok(()) |
| 511 | } |
| 512 |
| 513 | fn save_row(row: &rusqlite::Row) -> rusqlite::Result<Save> { |
| 514 | Ok(Save { |
| 515 | change: row.get(0)?, |
| 516 | n: row.get(1)?, |
| 517 | tree: id_col(row, 2)?, |
| 518 | base_tree: id_col(row, 3)?, |
| 519 | at: row.get(4)?, |
| 520 | uploaded: row.get::<_, i64>(5)? != 0, |
| 521 | }) |
| 522 | } |
| 523 |
| 524 | pub fn saves(&self, change: &str) -> Result<Vec<Save>> { |
| 525 | let mut stmt = self |
| 526 | .conn |
| 527 | .prepare("SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE change = ?1 ORDER BY n")?; |
| 528 | let rows = stmt.query_map([change], Self::save_row)?; |
| 529 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 530 | } |
| 531 |
| 532 | pub fn save(&self, change: &str, n: i64) -> Result<Option<Save>> { |
| 533 | Ok(self |
| 534 | .conn |
| 535 | .query_row( |
| 536 | "SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE change = ?1 AND n = ?2", |
| 537 | params![change, n], |
| 538 | Self::save_row, |
| 539 | ) |
| 540 | .optional()?) |
| 541 | } |
| 542 |
| 543 | /// The latest save made in `workspace` at or before `at`, across changes. |
| 544 | pub fn save_at(&self, workspace: &str, at: i64) -> Result<Option<Save>> { |
| 545 | Ok(self |
| 546 | .conn |
| 547 | .query_row( |
| 548 | "SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE workspace = ?1 AND at <= ?2 |
| 549 | ORDER BY at DESC, rowid DESC LIMIT 1", |
| 550 | params![workspace, at], |
| 551 | Self::save_row, |
| 552 | ) |
| 553 | .optional()?) |
| 554 | } |
| 555 |
| 556 | pub fn unuploaded_saves(&self, change: &str) -> Result<Vec<Save>> { |
| 557 | let mut stmt = self.conn.prepare( |
| 558 | "SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE change = ?1 AND uploaded = 0 ORDER BY n", |
| 559 | )?; |
| 560 | let rows = stmt.query_map([change], Self::save_row)?; |
| 561 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 562 | } |
| 563 |
| 564 | pub fn mark_uploaded(&self, change: &str, n: i64) -> Result<()> { |
| 565 | self.conn.execute("UPDATE saves SET uploaded = 1 WHERE change = ?1 AND n = ?2", params![change, n])?; |
| 566 | Ok(()) |
| 567 | } |
| 568 |
| 569 | /// Drops saves (the secret scrub): every save of `change` goes, and the |
| 570 | /// caller records a fresh one. |
| 571 | pub fn delete_saves(&self, change: &str) -> Result<()> { |
| 572 | self.conn.execute("DELETE FROM saves WHERE change = ?1", [change])?; |
| 573 | Ok(()) |
| 574 | } |
| 575 |
| 576 | // Operations. |
| 577 |
| 578 | pub fn add_op(&self, workspace: &str, kind: &str, data: &serde_json::Value) -> Result<i64> { |
| 579 | self.conn.execute( |
| 580 | "INSERT INTO ops (workspace, at, kind, data) VALUES (?1, ?2, ?3, ?4)", |
| 581 | params![workspace, ids::now(), kind, data.to_string()], |
| 582 | )?; |
| 583 | Ok(self.conn.last_insert_rowid()) |
| 584 | } |
| 585 |
| 586 | fn op_row(row: &rusqlite::Row) -> rusqlite::Result<Op> { |
| 587 | let data: String = row.get(3)?; |
| 588 | Ok(Op { id: row.get(0)?, at: row.get(1)?, kind: row.get(2)?, data: serde_json::from_str(&data).unwrap_or_default() }) |
| 589 | } |
| 590 |
| 591 | pub fn last_op(&self, workspace: &str) -> Result<Option<Op>> { |
| 592 | Ok(self |
| 593 | .conn |
| 594 | .query_row( |
| 595 | "SELECT id, at, kind, data FROM ops WHERE workspace = ?1 AND undone = 0 ORDER BY id DESC LIMIT 1", |
| 596 | [workspace], |
| 597 | Self::op_row, |
| 598 | ) |
| 599 | .optional()?) |
| 600 | } |
| 601 |
| 602 | pub fn ops(&self, workspace: &str, limit: i64) -> Result<Vec<(Op, bool)>> { |
| 603 | let mut stmt = |
| 604 | self.conn.prepare("SELECT id, at, kind, data, undone FROM ops WHERE workspace = ?1 ORDER BY id DESC LIMIT ?2")?; |
| 605 | let rows = stmt.query_map(params![workspace, limit], |row| Ok((Self::op_row(row)?, row.get::<_, i64>(4)? != 0)))?; |
| 606 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 607 | } |
| 608 |
| 609 | pub fn mark_undone(&self, id: i64) -> Result<()> { |
| 610 | self.conn.execute("UPDATE ops SET undone = 1 WHERE id = ?1", [id])?; |
| 611 | Ok(()) |
| 612 | } |
| 613 |
| 614 | // Mine files: what the user's own files looked like when last saved. |
| 615 |
| 616 | pub fn put_mine(&self, project: &str, path: &str, file: &Id, exec: bool) -> Result<()> { |
| 617 | self.conn.execute( |
| 618 | "INSERT OR REPLACE INTO mine (project, path, file, exec, at) VALUES (?1, ?2, ?3, ?4, ?5)", |
| 619 | params![project, path, file.hex(), exec as i64, ids::now()], |
| 620 | )?; |
| 621 | Ok(()) |
| 622 | } |
| 623 |
| 624 | pub fn mine(&self, project: &str) -> Result<Vec<(String, Id, bool)>> { |
| 625 | let mut stmt = self.conn.prepare("SELECT path, file, exec FROM mine WHERE project = ?1 ORDER BY path")?; |
| 626 | let rows = stmt.query_map([project], |r| Ok((r.get::<_, String>(0)?, id_col(r, 1)?, r.get::<_, i64>(2)? != 0)))?; |
| 627 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 628 | } |
| 629 | } |
| 630 |