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 | -- The first version of main this machine keeps, when it isn't 1: a new |
| 26 | -- machine fetches recent history, and older versions when asked for. |
| 27 | CREATE TABLE IF NOT EXISTS history ( |
| 28 | project TEXT PRIMARY KEY, |
| 29 | start INTEGER NOT NULL |
| 30 | ); |
| 31 | CREATE TABLE IF NOT EXISTS versions ( |
| 32 | project TEXT NOT NULL, |
| 33 | n INTEGER NOT NULL, |
| 34 | id TEXT NOT NULL, |
| 35 | tree TEXT NOT NULL, |
| 36 | prev TEXT, |
| 37 | change TEXT NOT NULL, |
| 38 | author TEXT NOT NULL, |
| 39 | time INTEGER NOT NULL, |
| 40 | title TEXT NOT NULL, |
| 41 | body TEXT NOT NULL, |
| 42 | git TEXT, |
| 43 | PRIMARY KEY (project, n) |
| 44 | ); |
| 45 | CREATE INDEX IF NOT EXISTS versions_change ON versions(project, change); |
| 46 | CREATE TABLE IF NOT EXISTS workspaces ( |
| 47 | id TEXT PRIMARY KEY, |
| 48 | project TEXT NOT NULL, |
| 49 | path TEXT NOT NULL UNIQUE, |
| 50 | change TEXT NOT NULL, |
| 51 | only TEXT NOT NULL DEFAULT '[]', |
| 52 | created INTEGER NOT NULL, |
| 53 | used INTEGER NOT NULL |
| 54 | ); |
| 55 | CREATE TABLE IF NOT EXISTS changes ( |
| 56 | id TEXT PRIMARY KEY, |
| 57 | project TEXT NOT NULL, |
| 58 | title TEXT NOT NULL DEFAULT '', |
| 59 | body TEXT NOT NULL DEFAULT '', |
| 60 | base_tree TEXT NOT NULL, |
| 61 | base_version INTEGER, |
| 62 | on_change TEXT, |
| 63 | after_change TEXT, |
| 64 | head_tree TEXT, |
| 65 | saves INTEGER NOT NULL DEFAULT 0, |
| 66 | state TEXT NOT NULL DEFAULT 'open', |
| 67 | version INTEGER, |
| 68 | author TEXT NOT NULL DEFAULT '', |
| 69 | conflicts TEXT NOT NULL DEFAULT '[]', |
| 70 | workspace TEXT, |
| 71 | shared TEXT NOT NULL DEFAULT '[]', |
| 72 | remote_rev INTEGER NOT NULL DEFAULT 0, |
| 73 | created INTEGER NOT NULL, |
| 74 | updated INTEGER NOT NULL |
| 75 | ); |
| 76 | CREATE INDEX IF NOT EXISTS changes_project ON changes(project, state); |
| 77 | CREATE TABLE IF NOT EXISTS saves ( |
| 78 | change TEXT NOT NULL, |
| 79 | n INTEGER NOT NULL, |
| 80 | tree TEXT NOT NULL, |
| 81 | base_tree TEXT NOT NULL, |
| 82 | at INTEGER NOT NULL, |
| 83 | workspace TEXT, |
| 84 | uploaded INTEGER NOT NULL DEFAULT 0, |
| 85 | PRIMARY KEY (change, n) |
| 86 | ); |
| 87 | CREATE INDEX IF NOT EXISTS saves_time ON saves(workspace, at); |
| 88 | CREATE TABLE IF NOT EXISTS ops ( |
| 89 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| 90 | workspace TEXT NOT NULL, |
| 91 | at INTEGER NOT NULL, |
| 92 | kind TEXT NOT NULL, |
| 93 | data TEXT NOT NULL, |
| 94 | undone INTEGER NOT NULL DEFAULT 0 |
| 95 | ); |
| 96 | CREATE INDEX IF NOT EXISTS ops_workspace ON ops(workspace, id); |
| 97 | CREATE TABLE IF NOT EXISTS complete ( |
| 98 | tree TEXT PRIMARY KEY |
| 99 | ); |
| 100 | CREATE TABLE IF NOT EXISTS personal ( |
| 101 | project TEXT PRIMARY KEY, |
| 102 | patterns TEXT NOT NULL |
| 103 | ); |
| 104 | CREATE TABLE IF NOT EXISTS mine ( |
| 105 | project TEXT NOT NULL, |
| 106 | path TEXT NOT NULL, |
| 107 | file TEXT NOT NULL, |
| 108 | exec INTEGER NOT NULL DEFAULT 0, |
| 109 | at INTEGER NOT NULL, |
| 110 | PRIMARY KEY (project, path) |
| 111 | ); |
| 112 | CREATE INDEX IF NOT EXISTS versions_git ON versions(project, git); |
| 113 | CREATE INDEX IF NOT EXISTS versions_tree ON versions(project, tree); |
| 114 | CREATE TABLE IF NOT EXISTS imports ( |
| 115 | project TEXT PRIMARY KEY, |
| 116 | source TEXT NOT NULL, |
| 117 | branch TEXT NOT NULL |
| 118 | ); |
| 119 | CREATE TABLE IF NOT EXISTS uploaded ( |
| 120 | project TEXT NOT NULL, |
| 121 | tree TEXT NOT NULL, |
| 122 | PRIMARY KEY (project, tree) |
| 123 | ); |
| 124 | CREATE TABLE IF NOT EXISTS git_ids ( |
| 125 | project TEXT NOT NULL, |
| 126 | native TEXT NOT NULL, |
| 127 | git TEXT NOT NULL, |
| 128 | PRIMARY KEY (project, native) |
| 129 | ); |
| 130 | -- Names for versions of main (vcs tag), for local projects and vcs-server. |
| 131 | CREATE TABLE IF NOT EXISTS tags ( |
| 132 | project TEXT NOT NULL, |
| 133 | name TEXT NOT NULL, |
| 134 | n INTEGER NOT NULL, |
| 135 | by TEXT NOT NULL, |
| 136 | time INTEGER NOT NULL, |
| 137 | PRIMARY KEY (project, name) |
| 138 | ); |
| 139 | -- Release lines: named lines of versions next to main, each from a version |
| 140 | -- of main, with their own numbered versions (line_versions). |
| 141 | CREATE TABLE IF NOT EXISTS lines ( |
| 142 | project TEXT NOT NULL, |
| 143 | name TEXT NOT NULL, |
| 144 | from_n INTEGER NOT NULL, |
| 145 | created INTEGER NOT NULL, |
| 146 | PRIMARY KEY (project, name) |
| 147 | ); |
| 148 | CREATE TABLE IF NOT EXISTS line_versions ( |
| 149 | project TEXT NOT NULL, |
| 150 | line TEXT NOT NULL, |
| 151 | n INTEGER NOT NULL, |
| 152 | tree TEXT NOT NULL, |
| 153 | change TEXT NOT NULL, |
| 154 | author TEXT NOT NULL, |
| 155 | time INTEGER NOT NULL, |
| 156 | title TEXT NOT NULL, |
| 157 | body TEXT NOT NULL, |
| 158 | PRIMARY KEY (project, line, n) |
| 159 | ); |
| 160 | -- Changes the host sent through review: their change request (number and link). |
| 161 | CREATE TABLE IF NOT EXISTS reviews ( |
| 162 | change TEXT PRIMARY KEY, |
| 163 | link TEXT NOT NULL |
| 164 | ); |
| 165 | -- vcs purge on a host: how many purges the host has done, and how many this |
| 166 | -- machine has taken in (taking out the paths here too). |
| 167 | CREATE TABLE IF NOT EXISTS purges ( |
| 168 | project TEXT PRIMARY KEY, |
| 169 | seen INTEGER NOT NULL DEFAULT 0, |
| 170 | host INTEGER NOT NULL DEFAULT 0 |
| 171 | ); |
| 172 | -- Agent sessions kept with the changes they made (vcs sessions): each is the |
| 173 | -- part of the agent's session since the last one kept, in the store. |
| 174 | CREATE TABLE IF NOT EXISTS sessions ( |
| 175 | change TEXT NOT NULL, |
| 176 | n INTEGER NOT NULL, |
| 177 | agent TEXT NOT NULL, |
| 178 | file TEXT NOT NULL, |
| 179 | bytes INTEGER NOT NULL, |
| 180 | messages INTEGER NOT NULL, |
| 181 | at INTEGER NOT NULL, |
| 182 | PRIMARY KEY (change, n) |
| 183 | ); |
| 184 | -- How far into each agent session was kept already. |
| 185 | CREATE TABLE IF NOT EXISTS session_marks ( |
| 186 | session TEXT PRIMARY KEY, |
| 187 | size INTEGER NOT NULL |
| 188 | ); |
| 189 | -- Sealed folders (vcs access <path> --sealed) and the keys this machine has |
| 190 | -- for them (seal.rs); `gen` is the host's count of seal changes last read. |
| 191 | CREATE TABLE IF NOT EXISTS seal_folders ( |
| 192 | project TEXT NOT NULL, |
| 193 | path TEXT NOT NULL, |
| 194 | PRIMARY KEY (project, path) |
| 195 | ); |
| 196 | CREATE TABLE IF NOT EXISTS seal_keys ( |
| 197 | project TEXT NOT NULL, |
| 198 | path TEXT NOT NULL, |
| 199 | kid TEXT NOT NULL, |
| 200 | key TEXT NOT NULL, |
| 201 | at INTEGER NOT NULL, |
| 202 | PRIMARY KEY (project, path, kid) |
| 203 | ); |
| 204 | CREATE TABLE IF NOT EXISTS seal_state ( |
| 205 | project TEXT PRIMARY KEY, |
| 206 | seen INTEGER NOT NULL DEFAULT 0, |
| 207 | host INTEGER NOT NULL DEFAULT 0 |
| 208 | ); |
| 209 | "#; |
| 210 |
| 211 | pub struct Db { |
| 212 | pub conn: Connection, |
| 213 | } |
| 214 |
| 215 | #[derive(Debug, Clone)] |
| 216 | pub struct Project { |
| 217 | pub id: String, |
| 218 | /// `host/owner/name` for hosted projects, the folder for local ones. |
| 219 | pub address: String, |
| 220 | /// "local" or "hosted". |
| 221 | pub kind: String, |
| 222 | pub api: Option<String>, |
| 223 | /// When main was last checked with the host (seconds). |
| 224 | pub checked: i64, |
| 225 | } |
| 226 |
| 227 | impl Project { |
| 228 | pub fn hosted(&self) -> bool { |
| 229 | self.kind == "hosted" |
| 230 | } |
| 231 | } |
| 232 |
| 233 | /// A release line: from version `from` of main, with versions 1 to `head` of its own. |
| 234 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 235 | pub struct Line { |
| 236 | pub name: String, |
| 237 | pub from: u64, |
| 238 | pub head: u64, |
| 239 | } |
| 240 |
| 241 | /// An agent session kept with a change: the part of it since the last one kept. |
| 242 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 243 | pub struct SessionRow { |
| 244 | #[serde(default)] |
| 245 | pub n: u64, |
| 246 | /// "Claude Code", "Codex", or what VCS_SESSION_AGENT says. |
| 247 | pub agent: String, |
| 248 | /// The session's text (a file in the store), secrets removed. |
| 249 | pub file: Id, |
| 250 | pub bytes: u64, |
| 251 | pub messages: u64, |
| 252 | #[serde(default)] |
| 253 | pub at: i64, |
| 254 | } |
| 255 |
| 256 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 257 | pub struct LineVersion { |
| 258 | pub line: String, |
| 259 | pub n: u64, |
| 260 | pub tree: Id, |
| 261 | pub change: String, |
| 262 | pub author: String, |
| 263 | pub time: i64, |
| 264 | pub title: String, |
| 265 | pub body: String, |
| 266 | } |
| 267 |
| 268 | #[derive(Debug, Clone)] |
| 269 | pub struct Workspace { |
| 270 | pub id: String, |
| 271 | pub project: String, |
| 272 | pub path: PathBuf, |
| 273 | pub change: String, |
| 274 | pub only: Vec<String>, |
| 275 | } |
| 276 |
| 277 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| 278 | pub struct Conflict { |
| 279 | pub path: String, |
| 280 | #[serde(default)] |
| 281 | pub version: Option<u64>, |
| 282 | } |
| 283 |
| 284 | #[derive(Debug, Clone)] |
| 285 | pub struct Change { |
| 286 | pub id: String, |
| 287 | pub project: String, |
| 288 | pub title: String, |
| 289 | pub body: String, |
| 290 | /// The tree the change starts from. |
| 291 | pub base_tree: Id, |
| 292 | /// Set when the base is a version of main. |
| 293 | pub base_version: Option<u64>, |
| 294 | /// Set when stacked on an unlanded change. |
| 295 | pub on_change: Option<String>, |
| 296 | /// Set when the change continues after one that landed from the same workspace. |
| 297 | pub after_change: Option<String>, |
| 298 | /// The latest save (None: nothing saved, same as the base). |
| 299 | pub head_tree: Option<Id>, |
| 300 | pub saves: i64, |
| 301 | /// open, waiting, landed, dropped. |
| 302 | pub state: String, |
| 303 | pub version: Option<u64>, |
| 304 | pub author: String, |
| 305 | pub conflicts: Vec<Conflict>, |
| 306 | pub workspace: Option<String>, |
| 307 | pub shared: Vec<String>, |
| 308 | pub remote_rev: i64, |
| 309 | pub created: i64, |
| 310 | pub updated: i64, |
| 311 | /// The release line it lands on (None: main). |
| 312 | pub line: Option<String>, |
| 313 | } |
| 314 |
| 315 | impl Change { |
| 316 | pub fn new(id: String, project: &str, base_tree: Id, base_version: Option<u64>, author: &str) -> Change { |
| 317 | let now = ids::now(); |
| 318 | Change { |
| 319 | id, |
| 320 | project: project.to_string(), |
| 321 | title: String::new(), |
| 322 | body: String::new(), |
| 323 | base_tree, |
| 324 | base_version, |
| 325 | on_change: None, |
| 326 | after_change: None, |
| 327 | head_tree: None, |
| 328 | saves: 0, |
| 329 | state: "open".into(), |
| 330 | version: None, |
| 331 | author: author.to_string(), |
| 332 | conflicts: Vec::new(), |
| 333 | workspace: None, |
| 334 | shared: Vec::new(), |
| 335 | remote_rev: 0, |
| 336 | created: now, |
| 337 | updated: now, |
| 338 | line: None, |
| 339 | } |
| 340 | } |
| 341 |
| 342 | /// The change's content: its latest save, or its base. |
| 343 | pub fn tree(&self) -> Id { |
| 344 | self.head_tree.unwrap_or(self.base_tree) |
| 345 | } |
| 346 |
| 347 | pub fn is_empty(&self) -> bool { |
| 348 | self.tree() == self.base_tree |
| 349 | } |
| 350 |
| 351 | pub fn message(&self) -> String { |
| 352 | if self.body.is_empty() { self.title.clone() } else { format!("{}\n\n{}", self.title, self.body) } |
| 353 | } |
| 354 | } |
| 355 |
| 356 | #[derive(Debug, Clone)] |
| 357 | pub struct Save { |
| 358 | pub change: String, |
| 359 | pub n: i64, |
| 360 | pub tree: Id, |
| 361 | pub base_tree: Id, |
| 362 | pub at: i64, |
| 363 | pub uploaded: bool, |
| 364 | } |
| 365 |
| 366 | #[derive(Debug, Clone)] |
| 367 | pub struct Op { |
| 368 | pub id: i64, |
| 369 | pub at: i64, |
| 370 | pub kind: String, |
| 371 | pub data: serde_json::Value, |
| 372 | } |
| 373 |
| 374 | fn id_col(row: &rusqlite::Row, i: usize) -> rusqlite::Result<Id> { |
| 375 | let text: String = row.get(i)?; |
| 376 | Id::parse(&text).ok_or_else(|| rusqlite::Error::InvalidColumnType(i, "id".into(), rusqlite::types::Type::Text)) |
| 377 | } |
| 378 |
| 379 | fn opt_id_col(row: &rusqlite::Row, i: usize) -> rusqlite::Result<Option<Id>> { |
| 380 | let text: Option<String> = row.get(i)?; |
| 381 | Ok(text.and_then(|t| Id::parse(&t))) |
| 382 | } |
| 383 |
| 384 | impl Db { |
| 385 | pub fn open(home: &Path) -> Result<Db> { |
| 386 | std::fs::create_dir_all(home).with_context(|| format!("creating {}", home.display()))?; |
| 387 | let conn = Connection::open(home.join("vcs.db")).context("opening the vcs database")?; |
| 388 | Self::setup(conn) |
| 389 | } |
| 390 |
| 391 | pub fn setup(conn: Connection) -> Result<Db> { |
| 392 | // Many agents on one machine share this database. SQLite's own |
| 393 | // busy wait sleeps up to 100 ms between tries; this one tries again |
| 394 | // within a millisecond, for up to a minute. |
| 395 | conn.busy_handler(Some(|tries: i32| { |
| 396 | std::thread::sleep(std::time::Duration::from_micros(300 + (tries as u64 % 5) * 150)); |
| 397 | tries < 120_000 |
| 398 | }))?; |
| 399 | conn.pragma_update(None, "journal_mode", "WAL")?; |
| 400 | conn.pragma_update(None, "synchronous", "NORMAL")?; |
| 401 | conn.execute_batch(SCHEMA)?; |
| 402 | // The paths each version changed (see history::fill_touched), for |
| 403 | // databases made before it. Another process may add it first. |
| 404 | let has: bool = conn.query_row("SELECT count(*) > 0 FROM pragma_table_info('versions') WHERE name = 'touched'", [], |r| r.get(0))?; |
| 405 | if !has { |
| 406 | if let Err(e) = conn.execute("ALTER TABLE versions ADD COLUMN touched TEXT", []) |
| 407 | && !e.to_string().contains("duplicate column") |
| 408 | { |
| 409 | return Err(e.into()); |
| 410 | } |
| 411 | conn.execute("CREATE INDEX IF NOT EXISTS versions_untouched ON versions(project, n) WHERE touched IS NULL", [])?; |
| 412 | } |
| 413 | // The release line a change lands on, for databases made before lines. |
| 414 | let has: bool = conn.query_row("SELECT count(*) > 0 FROM pragma_table_info('changes') WHERE name = 'line'", [], |r| r.get(0))?; |
| 415 | if !has |
| 416 | && let Err(e) = conn.execute("ALTER TABLE changes ADD COLUMN line TEXT", []) |
| 417 | && !e.to_string().contains("duplicate column") |
| 418 | { |
| 419 | return Err(e.into()); |
| 420 | } |
| 421 | Ok(Db { conn }) |
| 422 | } |
| 423 |
| 424 | // Projects. |
| 425 |
| 426 | pub fn put_project(&self, p: &Project) -> Result<()> { |
| 427 | self.conn.execute( |
| 428 | "INSERT INTO projects (id, address, kind, api, checked, created) VALUES (?1, ?2, ?3, ?4, ?5, ?6) |
| 429 | ON CONFLICT(id) DO UPDATE SET address=excluded.address, kind=excluded.kind, api=excluded.api, checked=excluded.checked", |
| 430 | params![p.id, p.address, p.kind, p.api, p.checked, ids::now()], |
| 431 | )?; |
| 432 | Ok(()) |
| 433 | } |
| 434 |
| 435 | fn project_row(row: &rusqlite::Row) -> rusqlite::Result<Project> { |
| 436 | Ok(Project { id: row.get(0)?, address: row.get(1)?, kind: row.get(2)?, api: row.get(3)?, checked: row.get(4)? }) |
| 437 | } |
| 438 |
| 439 | pub fn project(&self, id: &str) -> Result<Option<Project>> { |
| 440 | Ok(self |
| 441 | .conn |
| 442 | .query_row("SELECT id, address, kind, api, checked FROM projects WHERE id = ?1", [id], Self::project_row) |
| 443 | .optional()?) |
| 444 | } |
| 445 |
| 446 | pub fn project_by_address(&self, address: &str) -> Result<Option<Project>> { |
| 447 | Ok(self |
| 448 | .conn |
| 449 | .query_row("SELECT id, address, kind, api, checked FROM projects WHERE address = ?1", [address], Self::project_row) |
| 450 | .optional()?) |
| 451 | } |
| 452 |
| 453 | pub fn set_checked(&self, project: &str, at: i64) -> Result<()> { |
| 454 | self.conn.execute("UPDATE projects SET checked = ?2 WHERE id = ?1", params![project, at])?; |
| 455 | Ok(()) |
| 456 | } |
| 457 |
| 458 | // Agent sessions (vcs sessions). |
| 459 |
| 460 | pub fn add_session(&self, change: &str, s: &SessionRow) -> Result<u64> { |
| 461 | let n: i64 = self.conn.query_row("SELECT coalesce(max(n), 0) + 1 FROM sessions WHERE change = ?1", [change], |r| r.get(0))?; |
| 462 | let n = if s.n > 0 { s.n as i64 } else { n }; |
| 463 | self.conn.execute( |
| 464 | "INSERT OR REPLACE INTO sessions (change, n, agent, file, bytes, messages, at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", |
| 465 | params![change, n, s.agent, s.file.hex(), s.bytes as i64, s.messages as i64, s.at], |
| 466 | )?; |
| 467 | Ok(n as u64) |
| 468 | } |
| 469 |
| 470 | pub fn sessions(&self, change: &str) -> Result<Vec<SessionRow>> { |
| 471 | let mut stmt = self.conn.prepare("SELECT n, agent, file, bytes, messages, at FROM sessions WHERE change = ?1 ORDER BY n")?; |
| 472 | let rows = stmt.query_map([change], |r| { |
| 473 | Ok(SessionRow { n: r.get::<_, i64>(0)? as u64, agent: r.get(1)?, file: id_col(r, 2)?, bytes: r.get::<_, i64>(3)? as u64, messages: r.get::<_, i64>(4)? as u64, at: r.get(5)? }) |
| 474 | })?; |
| 475 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 476 | } |
| 477 |
| 478 | /// Sessions kept with `project`'s changes here: (change, session). |
| 479 | pub fn project_sessions(&self, project: &str) -> Result<Vec<(String, SessionRow)>> { |
| 480 | let mut stmt = self.conn.prepare("SELECT s.change, s.n, s.agent, s.file, s.bytes, s.messages, s.at FROM sessions s JOIN changes c ON c.id = s.change WHERE c.project = ?1 ORDER BY s.at DESC")?; |
| 481 | let rows = stmt.query_map([project], |r| { |
| 482 | Ok((r.get(0)?, SessionRow { n: r.get::<_, i64>(1)? as u64, agent: r.get(2)?, file: id_col(r, 3)?, bytes: r.get::<_, i64>(4)? as u64, messages: r.get::<_, i64>(5)? as u64, at: r.get(6)? })) |
| 483 | })?; |
| 484 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 485 | } |
| 486 |
| 487 | /// Forgets sessions: one change's, or (None) every one of `project`'s changes'. |
| 488 | pub fn delete_sessions(&self, project: &str, change: Option<&str>) -> Result<usize> { |
| 489 | Ok(match change { |
| 490 | Some(c) => self.conn.execute("DELETE FROM sessions WHERE change = ?1", [c])?, |
| 491 | None => self.conn.execute("DELETE FROM sessions WHERE change IN (SELECT id FROM changes WHERE project = ?1)", [project])?, |
| 492 | }) |
| 493 | } |
| 494 |
| 495 | /// Every session file kept here (they stay through vcs gc). |
| 496 | pub fn session_files(&self) -> Result<Vec<Id>> { |
| 497 | let mut stmt = self.conn.prepare("SELECT file FROM sessions")?; |
| 498 | let rows = stmt.query_map([], |r| id_col(r, 0))?; |
| 499 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 500 | } |
| 501 |
| 502 | pub fn session_mark(&self, session: &str) -> Result<u64> { |
| 503 | let size: Option<i64> = self.conn.query_row("SELECT size FROM session_marks WHERE session = ?1", [session], |r| r.get(0)).optional()?; |
| 504 | Ok(size.unwrap_or(0).max(0) as u64) |
| 505 | } |
| 506 |
| 507 | pub fn set_session_mark(&self, session: &str, size: u64) -> Result<()> { |
| 508 | self.conn.execute("INSERT OR REPLACE INTO session_marks (session, size) VALUES (?1, ?2)", params![session, size as i64])?; |
| 509 | Ok(()) |
| 510 | } |
| 511 |
| 512 | // Purges (vcs purge): (taken in here, done on the host). |
| 513 |
| 514 | pub fn purges(&self, project: &str) -> Result<(u64, u64)> { |
| 515 | let row: Option<(i64, i64)> = self.conn.query_row("SELECT seen, host FROM purges WHERE project = ?1", [project], |r| Ok((r.get(0)?, r.get(1)?))).optional()?; |
| 516 | Ok(row.map_or((0, 0), |(s, h)| (s as u64, h as u64))) |
| 517 | } |
| 518 |
| 519 | pub fn set_purges(&self, project: &str, seen: Option<u64>, host: Option<u64>) -> Result<()> { |
| 520 | let (s, h) = self.purges(project)?; |
| 521 | self.conn.execute( |
| 522 | "INSERT OR REPLACE INTO purges (project, seen, host) VALUES (?1, ?2, ?3)", |
| 523 | params![project, seen.unwrap_or(s) as i64, host.unwrap_or(h).max(seen.unwrap_or(s)) as i64], |
| 524 | )?; |
| 525 | Ok(()) |
| 526 | } |
| 527 |
| 528 | // Versions. |
| 529 |
| 530 | /// Forgets versions after `n` (the host's project was rolled back). |
| 531 | pub fn drop_versions_after(&self, project: &str, n: u64) -> Result<usize> { |
| 532 | Ok(self.conn.execute("DELETE FROM versions WHERE project = ?1 AND n > ?2", params![project, n as i64])?) |
| 533 | } |
| 534 |
| 535 | pub fn put_version(&self, project: &str, v: &Version) -> Result<()> { |
| 536 | self.conn.execute( |
| 537 | "INSERT OR REPLACE INTO versions (project, n, id, tree, prev, change, author, time, title, body, git) |
| 538 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", |
| 539 | params![ |
| 540 | project, |
| 541 | v.n as i64, |
| 542 | v.id(project).hex(), |
| 543 | v.tree.hex(), |
| 544 | v.prev.map(|p| p.hex()), |
| 545 | v.change, |
| 546 | v.author, |
| 547 | v.time, |
| 548 | v.title, |
| 549 | v.body, |
| 550 | v.git |
| 551 | ], |
| 552 | )?; |
| 553 | Ok(()) |
| 554 | } |
| 555 |
| 556 | fn version_row(row: &rusqlite::Row) -> rusqlite::Result<Version> { |
| 557 | Ok(Version { |
| 558 | n: row.get::<_, i64>(0)? as u64, |
| 559 | tree: id_col(row, 1)?, |
| 560 | prev: opt_id_col(row, 2)?, |
| 561 | change: row.get(3)?, |
| 562 | author: row.get(4)?, |
| 563 | time: row.get(5)?, |
| 564 | title: row.get(6)?, |
| 565 | body: row.get(7)?, |
| 566 | git: row.get(8)?, |
| 567 | }) |
| 568 | } |
| 569 |
| 570 | const VERSION_COLUMNS: &'static str = "n, tree, prev, change, author, time, title, body, git"; |
| 571 |
| 572 | pub fn version(&self, project: &str, n: u64) -> Result<Option<Version>> { |
| 573 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 AND n = ?2", Self::VERSION_COLUMNS); |
| 574 | Ok(self.conn.query_row(&sql, params![project, n as i64], Self::version_row).optional()?) |
| 575 | } |
| 576 |
| 577 | pub fn latest_version(&self, project: &str) -> Result<Option<Version>> { |
| 578 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 ORDER BY n DESC LIMIT 1", Self::VERSION_COLUMNS); |
| 579 | Ok(self.conn.query_row(&sql, [project], Self::version_row).optional()?) |
| 580 | } |
| 581 |
| 582 | // Tags. |
| 583 |
| 584 | pub fn tags(&self, project: &str) -> Result<Vec<crate::tags::Tag>> { |
| 585 | let mut stmt = self.conn.prepare("SELECT name, n, by, time FROM tags WHERE project = ?1 ORDER BY n DESC, name")?; |
| 586 | let rows = stmt.query_map([project], |r| Ok(crate::tags::Tag { name: r.get(0)?, n: r.get::<_, i64>(1)? as u64, by: r.get(2)?, time: r.get(3)? }))?; |
| 587 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 588 | } |
| 589 |
| 590 | pub fn tag(&self, project: &str, name: &str) -> Result<Option<crate::tags::Tag>> { |
| 591 | Ok(self |
| 592 | .conn |
| 593 | .query_row("SELECT name, n, by, time FROM tags WHERE project = ?1 AND name = ?2", params![project, name], |r| { |
| 594 | Ok(crate::tags::Tag { name: r.get(0)?, n: r.get::<_, i64>(1)? as u64, by: r.get(2)?, time: r.get(3)? }) |
| 595 | }) |
| 596 | .optional()?) |
| 597 | } |
| 598 |
| 599 | pub fn put_tag(&self, project: &str, t: &crate::tags::Tag) -> Result<()> { |
| 600 | self.conn.execute("INSERT INTO tags (project, name, n, by, time) VALUES (?1, ?2, ?3, ?4, ?5)", params![project, t.name, t.n as i64, t.by, t.time])?; |
| 601 | Ok(()) |
| 602 | } |
| 603 |
| 604 | pub fn drop_tag(&self, project: &str, name: &str) -> Result<bool> { |
| 605 | Ok(self.conn.execute("DELETE FROM tags WHERE project = ?1 AND name = ?2", params![project, name])? > 0) |
| 606 | } |
| 607 |
| 608 | // Release lines. |
| 609 |
| 610 | pub fn lines(&self, project: &str) -> Result<Vec<Line>> { |
| 611 | let mut stmt = self.conn.prepare( |
| 612 | "SELECT l.name, l.from_n, (SELECT max(n) FROM line_versions v WHERE v.project = l.project AND v.line = l.name) FROM lines l WHERE l.project = ?1 ORDER BY l.created", |
| 613 | )?; |
| 614 | let rows = stmt.query_map([project], |r| Ok(Line { name: r.get(0)?, from: r.get::<_, i64>(1)? as u64, head: r.get::<_, Option<i64>>(2)?.unwrap_or(0) as u64 }))?; |
| 615 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 616 | } |
| 617 |
| 618 | pub fn line(&self, project: &str, name: &str) -> Result<Option<Line>> { |
| 619 | Ok(self.lines(project)?.into_iter().find(|l| l.name == name)) |
| 620 | } |
| 621 |
| 622 | pub fn put_line(&self, project: &str, name: &str, from: u64) -> Result<()> { |
| 623 | self.conn.execute("INSERT OR IGNORE INTO lines (project, name, from_n, created) VALUES (?1, ?2, ?3, ?4)", params![project, name, from as i64, ids::now()])?; |
| 624 | Ok(()) |
| 625 | } |
| 626 |
| 627 | /// The tree at a line's version `n` (0: the version of main it started from). |
| 628 | pub fn line_tree(&self, project: &str, line: &Line, n: u64) -> Result<Option<Id>> { |
| 629 | if n == 0 { |
| 630 | return self.version_tree(project, line.from); |
| 631 | } |
| 632 | Ok(self.conn.query_row("SELECT tree FROM line_versions WHERE project = ?1 AND line = ?2 AND n = ?3", params![project, line.name, n as i64], |r| id_col(r, 0)).optional()?) |
| 633 | } |
| 634 |
| 635 | pub fn put_line_version(&self, project: &str, v: &LineVersion) -> Result<()> { |
| 636 | self.conn.execute( |
| 637 | "INSERT OR REPLACE INTO line_versions (project, line, n, tree, change, author, time, title, body) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", |
| 638 | params![project, v.line, v.n as i64, v.tree.hex(), v.change, v.author, v.time, v.title, v.body], |
| 639 | )?; |
| 640 | Ok(()) |
| 641 | } |
| 642 |
| 643 | /// A line's versions, newest first. |
| 644 | pub fn line_versions(&self, project: &str, line: &str, limit: i64) -> Result<Vec<LineVersion>> { |
| 645 | let mut stmt = self.conn.prepare("SELECT line, n, tree, change, author, time, title, body FROM line_versions WHERE project = ?1 AND line = ?2 ORDER BY n DESC LIMIT ?3")?; |
| 646 | let rows = stmt.query_map(params![project, line, limit], |r| { |
| 647 | Ok(LineVersion { line: r.get(0)?, n: r.get::<_, i64>(1)? as u64, tree: id_col(r, 2)?, change: r.get(3)?, author: r.get(4)?, time: r.get(5)?, title: r.get(6)?, body: r.get(7)? }) |
| 648 | })?; |
| 649 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 650 | } |
| 651 |
| 652 | /// Forgets a hosted project this machine only just looked up (it has no |
| 653 | /// workspaces here yet), so an import can take its address. |
| 654 | pub fn forget_project(&self, id: &str) -> Result<()> { |
| 655 | self.conn.execute("DELETE FROM versions WHERE project = ?1", [id])?; |
| 656 | self.conn.execute("DELETE FROM projects WHERE id = ?1", [id])?; |
| 657 | Ok(()) |
| 658 | } |
| 659 |
| 660 | /// The change request a change is in review as (None: it isn't). |
| 661 | pub fn review(&self, change: &str) -> Result<Option<String>> { |
| 662 | Ok(self.conn.query_row("SELECT link FROM reviews WHERE change = ?1", [change], |r| r.get(0)).optional()?) |
| 663 | } |
| 664 |
| 665 | pub fn set_review(&self, change: &str, link: Option<&str>) -> Result<()> { |
| 666 | match link { |
| 667 | Some(l) => self.conn.execute("INSERT OR REPLACE INTO reviews (change, link) VALUES (?1, ?2)", params![change, l])?, |
| 668 | None => self.conn.execute("DELETE FROM reviews WHERE change = ?1", [change])?, |
| 669 | }; |
| 670 | Ok(()) |
| 671 | } |
| 672 |
| 673 | pub fn version_tree(&self, project: &str, n: u64) -> Result<Option<Id>> { |
| 674 | Ok(self.conn.query_row("SELECT tree FROM versions WHERE project = ?1 AND n = ?2", params![project, n as i64], |r| id_col(r, 0)).optional()?) |
| 675 | } |
| 676 |
| 677 | /// Versions whose changed paths aren't worked out yet, newest first: |
| 678 | /// (n, tree, the tree of the version before it, when this machine has it). |
| 679 | pub fn untouched(&self, project: &str, limit: i64) -> Result<Vec<(u64, Id, Option<Id>)>> { |
| 680 | let mut stmt = self.conn.prepare( |
| 681 | "SELECT v.n, v.tree, (SELECT p.tree FROM versions p WHERE p.project = v.project AND p.n = v.n - 1) |
| 682 | FROM versions v WHERE v.project = ?1 AND v.touched IS NULL ORDER BY v.n DESC LIMIT ?2", |
| 683 | )?; |
| 684 | let rows = stmt.query_map(params![project, limit], |r| Ok((r.get::<_, i64>(0)? as u64, id_col(r, 1)?, opt_id_col(r, 2)?)))?; |
| 685 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 686 | } |
| 687 |
| 688 | pub fn set_touched(&self, project: &str, rows: &[(u64, String)]) -> Result<()> { |
| 689 | let tx = self.conn.unchecked_transaction()?; |
| 690 | { |
| 691 | let mut stmt = tx.prepare("UPDATE versions SET touched = ?3 WHERE project = ?1 AND n = ?2")?; |
| 692 | for (n, touched) in rows { |
| 693 | stmt.execute(params![project, *n as i64, touched])?; |
| 694 | } |
| 695 | } |
| 696 | tx.commit()?; |
| 697 | Ok(()) |
| 698 | } |
| 699 |
| 700 | /// Versions before `before` that may have changed `path`, newest first, |
| 701 | /// as (n, tree): their changed paths include it, something under it, or |
| 702 | /// a folder holding it, or weren't listed (too many, or not worked out). |
| 703 | pub fn may_touch(&self, project: &str, path: &str, before: u64, limit: i64) -> Result<Vec<(u64, Id)>> { |
| 704 | use rusqlite::types::Value; |
| 705 | let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect(); |
| 706 | let mut args: Vec<Value> = vec![project.to_string().into(), (before.min(i64::MAX as u64) as i64).into(), limit.into()]; |
| 707 | let filter = if parts.is_empty() { |
| 708 | "1".to_string() |
| 709 | } else { |
| 710 | let line = |i: usize| format!("instr(char(10) || touched || char(10), char(10) || ?{i} || char(10)) > 0"); |
| 711 | args.push(path.to_string().into()); |
| 712 | let mut any = vec!["touched IS NULL".to_string(), "touched = '*'".to_string(), line(4), "instr(char(10) || touched, char(10) || ?4 || '/') > 0".to_string()]; |
| 713 | for i in 1..parts.len() { |
| 714 | args.push(parts[..i].join("/").into()); |
| 715 | any.push(line(args.len())); |
| 716 | } |
| 717 | any.join(" OR ") |
| 718 | }; |
| 719 | let sql = format!("SELECT n, tree FROM versions WHERE project = ?1 AND n < ?2 AND ({filter}) ORDER BY n DESC LIMIT ?3"); |
| 720 | let mut stmt = self.conn.prepare(&sql)?; |
| 721 | let rows = stmt.query_map(rusqlite::params_from_iter(args), |r| Ok((r.get::<_, i64>(0)? as u64, id_col(r, 1)?)))?; |
| 722 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 723 | } |
| 724 |
| 725 | pub fn versions(&self, project: &str, limit: i64) -> Result<Vec<Version>> { |
| 726 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 ORDER BY n DESC LIMIT ?2", Self::VERSION_COLUMNS); |
| 727 | let mut stmt = self.conn.prepare(&sql)?; |
| 728 | let rows = stmt.query_map(params![project, limit], Self::version_row)?; |
| 729 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 730 | } |
| 731 |
| 732 | pub fn version_of_change(&self, project: &str, change: &str) -> Result<Option<Version>> { |
| 733 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 AND change = ?2 ORDER BY n DESC LIMIT 1", Self::VERSION_COLUMNS); |
| 734 | Ok(self.conn.query_row(&sql, params![project, change], Self::version_row).optional()?) |
| 735 | } |
| 736 |
| 737 | // Workspaces. |
| 738 |
| 739 | pub fn put_workspace(&self, w: &Workspace) -> Result<()> { |
| 740 | let now = ids::now(); |
| 741 | // A folder holds one workspace: any other one recorded at this path |
| 742 | // was deleted with its folder. Its change stays. |
| 743 | self.conn.execute("DELETE FROM workspaces WHERE path = ?1 AND id != ?2", params![w.path.to_string_lossy(), w.id])?; |
| 744 | self.conn.execute( |
| 745 | "INSERT INTO workspaces (id, project, path, change, only, created, used) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6) |
| 746 | ON CONFLICT(id) DO UPDATE SET project=excluded.project, path=excluded.path, change=excluded.change, |
| 747 | only=excluded.only, used=excluded.used", |
| 748 | params![w.id, w.project, w.path.to_string_lossy(), w.change, serde_json::to_string(&w.only)?, now], |
| 749 | )?; |
| 750 | Ok(()) |
| 751 | } |
| 752 |
| 753 | fn workspace_row(row: &rusqlite::Row) -> rusqlite::Result<Workspace> { |
| 754 | let only: String = row.get(4)?; |
| 755 | Ok(Workspace { |
| 756 | id: row.get(0)?, |
| 757 | project: row.get(1)?, |
| 758 | path: PathBuf::from(row.get::<_, String>(2)?), |
| 759 | change: row.get(3)?, |
| 760 | only: serde_json::from_str(&only).unwrap_or_default(), |
| 761 | }) |
| 762 | } |
| 763 |
| 764 | pub fn workspace(&self, id: &str) -> Result<Option<Workspace>> { |
| 765 | Ok(self |
| 766 | .conn |
| 767 | .query_row("SELECT id, project, path, change, only FROM workspaces WHERE id = ?1", [id], Self::workspace_row) |
| 768 | .optional()?) |
| 769 | } |
| 770 |
| 771 | pub fn project_workspaces(&self, project: &str) -> Result<Vec<Workspace>> { |
| 772 | let mut stmt = |
| 773 | self.conn.prepare("SELECT id, project, path, change, only FROM workspaces WHERE project = ?1 ORDER BY used DESC")?; |
| 774 | let rows = stmt.query_map([project], Self::workspace_row)?; |
| 775 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 776 | } |
| 777 |
| 778 | pub fn all_workspaces(&self) -> Result<Vec<Workspace>> { |
| 779 | let mut stmt = self.conn.prepare("SELECT id, project, path, change, only FROM workspaces ORDER BY used DESC")?; |
| 780 | let rows = stmt.query_map([], Self::workspace_row)?; |
| 781 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 782 | } |
| 783 |
| 784 | pub fn workspace_holding(&self, change: &str) -> Result<Option<Workspace>> { |
| 785 | Ok(self |
| 786 | .conn |
| 787 | .query_row( |
| 788 | "SELECT id, project, path, change, only FROM workspaces WHERE change = ?1 ORDER BY used DESC LIMIT 1", |
| 789 | [change], |
| 790 | Self::workspace_row, |
| 791 | ) |
| 792 | .optional()?) |
| 793 | } |
| 794 |
| 795 | pub fn delete_workspace(&self, id: &str) -> Result<()> { |
| 796 | self.conn.execute("DELETE FROM workspaces WHERE id = ?1", [id])?; |
| 797 | Ok(()) |
| 798 | } |
| 799 |
| 800 | // Changes. |
| 801 |
| 802 | pub fn put_change(&self, c: &Change) -> Result<()> { |
| 803 | self.conn.execute( |
| 804 | "INSERT INTO changes (id, project, title, body, base_tree, base_version, on_change, after_change, head_tree, |
| 805 | saves, state, version, author, conflicts, workspace, shared, remote_rev, created, updated, line) |
| 806 | VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20) |
| 807 | ON CONFLICT(id) DO UPDATE SET title=excluded.title, body=excluded.body, base_tree=excluded.base_tree, |
| 808 | base_version=excluded.base_version, on_change=excluded.on_change, after_change=excluded.after_change, |
| 809 | head_tree=excluded.head_tree, saves=excluded.saves, state=excluded.state, version=excluded.version, |
| 810 | author=excluded.author, conflicts=excluded.conflicts, workspace=excluded.workspace, |
| 811 | shared=excluded.shared, remote_rev=excluded.remote_rev, updated=excluded.updated, line=excluded.line", |
| 812 | params![ |
| 813 | c.id, |
| 814 | c.project, |
| 815 | c.title, |
| 816 | c.body, |
| 817 | c.base_tree.hex(), |
| 818 | c.base_version.map(|v| v as i64), |
| 819 | c.on_change, |
| 820 | c.after_change, |
| 821 | c.head_tree.map(|t| t.hex()), |
| 822 | c.saves, |
| 823 | c.state, |
| 824 | c.version.map(|v| v as i64), |
| 825 | c.author, |
| 826 | serde_json::to_string(&c.conflicts)?, |
| 827 | c.workspace, |
| 828 | serde_json::to_string(&c.shared)?, |
| 829 | c.remote_rev, |
| 830 | c.created, |
| 831 | ids::now(), |
| 832 | c.line |
| 833 | ], |
| 834 | )?; |
| 835 | Ok(()) |
| 836 | } |
| 837 |
| 838 | 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, line"; |
| 839 |
| 840 | fn change_row(row: &rusqlite::Row) -> rusqlite::Result<Change> { |
| 841 | let conflicts: String = row.get(13)?; |
| 842 | let shared: String = row.get(15)?; |
| 843 | Ok(Change { |
| 844 | id: row.get(0)?, |
| 845 | project: row.get(1)?, |
| 846 | title: row.get(2)?, |
| 847 | body: row.get(3)?, |
| 848 | base_tree: id_col(row, 4)?, |
| 849 | base_version: row.get::<_, Option<i64>>(5)?.map(|v| v as u64), |
| 850 | on_change: row.get(6)?, |
| 851 | after_change: row.get(7)?, |
| 852 | head_tree: opt_id_col(row, 8)?, |
| 853 | saves: row.get(9)?, |
| 854 | state: row.get(10)?, |
| 855 | version: row.get::<_, Option<i64>>(11)?.map(|v| v as u64), |
| 856 | author: row.get(12)?, |
| 857 | conflicts: serde_json::from_str(&conflicts).unwrap_or_default(), |
| 858 | workspace: row.get(14)?, |
| 859 | shared: serde_json::from_str(&shared).unwrap_or_default(), |
| 860 | remote_rev: row.get(16)?, |
| 861 | created: row.get(17)?, |
| 862 | updated: row.get(18)?, |
| 863 | line: row.get(19)?, |
| 864 | }) |
| 865 | } |
| 866 |
| 867 | pub fn change(&self, id: &str) -> Result<Option<Change>> { |
| 868 | let sql = format!("SELECT {} FROM changes WHERE id = ?1", Self::CHANGE_COLUMNS); |
| 869 | Ok(self.conn.query_row(&sql, [id], Self::change_row).optional()?) |
| 870 | } |
| 871 |
| 872 | pub fn changes(&self, project: &str) -> Result<Vec<Change>> { |
| 873 | let sql = format!("SELECT {} FROM changes WHERE project = ?1 ORDER BY updated DESC", Self::CHANGE_COLUMNS); |
| 874 | let mut stmt = self.conn.prepare(&sql)?; |
| 875 | let rows = stmt.query_map([project], Self::change_row)?; |
| 876 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 877 | } |
| 878 |
| 879 | pub fn change_ids(&self, project: &str) -> Result<Vec<String>> { |
| 880 | let mut stmt = self.conn.prepare("SELECT id FROM changes WHERE project = ?1")?; |
| 881 | let rows = stmt.query_map([project], |r| r.get::<_, String>(0))?; |
| 882 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 883 | } |
| 884 |
| 885 | pub fn changes_by_prefix(&self, project: &str, prefix: &str) -> Result<Vec<Change>> { |
| 886 | let sql = format!("SELECT {} FROM changes WHERE project = ?1 AND id LIKE ?2 || '%'", Self::CHANGE_COLUMNS); |
| 887 | let mut stmt = self.conn.prepare(&sql)?; |
| 888 | let rows = stmt.query_map(params![project, prefix], Self::change_row)?; |
| 889 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 890 | } |
| 891 |
| 892 | // Saves. |
| 893 |
| 894 | pub fn add_save(&self, s: &Save, workspace: &str) -> Result<()> { |
| 895 | self.conn.execute( |
| 896 | "INSERT OR REPLACE INTO saves (change, n, tree, base_tree, at, workspace, uploaded) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", |
| 897 | params![s.change, s.n, s.tree.hex(), s.base_tree.hex(), s.at, workspace, s.uploaded as i64], |
| 898 | )?; |
| 899 | Ok(()) |
| 900 | } |
| 901 |
| 902 | fn save_row(row: &rusqlite::Row) -> rusqlite::Result<Save> { |
| 903 | Ok(Save { |
| 904 | change: row.get(0)?, |
| 905 | n: row.get(1)?, |
| 906 | tree: id_col(row, 2)?, |
| 907 | base_tree: id_col(row, 3)?, |
| 908 | at: row.get(4)?, |
| 909 | uploaded: row.get::<_, i64>(5)? != 0, |
| 910 | }) |
| 911 | } |
| 912 |
| 913 | pub fn saves(&self, change: &str) -> Result<Vec<Save>> { |
| 914 | let mut stmt = self |
| 915 | .conn |
| 916 | .prepare("SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE change = ?1 ORDER BY n")?; |
| 917 | let rows = stmt.query_map([change], Self::save_row)?; |
| 918 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 919 | } |
| 920 |
| 921 | pub fn save(&self, change: &str, n: i64) -> Result<Option<Save>> { |
| 922 | Ok(self |
| 923 | .conn |
| 924 | .query_row( |
| 925 | "SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE change = ?1 AND n = ?2", |
| 926 | params![change, n], |
| 927 | Self::save_row, |
| 928 | ) |
| 929 | .optional()?) |
| 930 | } |
| 931 |
| 932 | /// The latest save made in `workspace` at or before `at`, across changes. |
| 933 | pub fn save_at(&self, workspace: &str, at: i64) -> Result<Option<Save>> { |
| 934 | Ok(self |
| 935 | .conn |
| 936 | .query_row( |
| 937 | "SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE workspace = ?1 AND at <= ?2 |
| 938 | ORDER BY at DESC, rowid DESC LIMIT 1", |
| 939 | params![workspace, at], |
| 940 | Self::save_row, |
| 941 | ) |
| 942 | .optional()?) |
| 943 | } |
| 944 |
| 945 | pub fn unuploaded_saves(&self, change: &str) -> Result<Vec<Save>> { |
| 946 | let mut stmt = self.conn.prepare( |
| 947 | "SELECT change, n, tree, base_tree, at, uploaded FROM saves WHERE change = ?1 AND uploaded = 0 ORDER BY n", |
| 948 | )?; |
| 949 | let rows = stmt.query_map([change], Self::save_row)?; |
| 950 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 951 | } |
| 952 |
| 953 | pub fn mark_uploaded(&self, change: &str, n: i64) -> Result<()> { |
| 954 | self.conn.execute("UPDATE saves SET uploaded = 1 WHERE change = ?1 AND n = ?2", params![change, n])?; |
| 955 | Ok(()) |
| 956 | } |
| 957 |
| 958 | /// Drops saves (the secret scrub): every save of `change` goes, and the |
| 959 | /// caller records a fresh one. |
| 960 | pub fn delete_saves(&self, change: &str) -> Result<()> { |
| 961 | self.conn.execute("DELETE FROM saves WHERE change = ?1", [change])?; |
| 962 | Ok(()) |
| 963 | } |
| 964 |
| 965 | // Operations. |
| 966 |
| 967 | pub fn add_op(&self, workspace: &str, kind: &str, data: &serde_json::Value) -> Result<i64> { |
| 968 | self.conn.execute( |
| 969 | "INSERT INTO ops (workspace, at, kind, data) VALUES (?1, ?2, ?3, ?4)", |
| 970 | params![workspace, ids::now(), kind, data.to_string()], |
| 971 | )?; |
| 972 | Ok(self.conn.last_insert_rowid()) |
| 973 | } |
| 974 |
| 975 | fn op_row(row: &rusqlite::Row) -> rusqlite::Result<Op> { |
| 976 | let data: String = row.get(3)?; |
| 977 | Ok(Op { id: row.get(0)?, at: row.get(1)?, kind: row.get(2)?, data: serde_json::from_str(&data).unwrap_or_default() }) |
| 978 | } |
| 979 |
| 980 | pub fn last_op(&self, workspace: &str) -> Result<Option<Op>> { |
| 981 | Ok(self |
| 982 | .conn |
| 983 | .query_row( |
| 984 | "SELECT id, at, kind, data FROM ops WHERE workspace = ?1 AND undone = 0 ORDER BY id DESC LIMIT 1", |
| 985 | [workspace], |
| 986 | Self::op_row, |
| 987 | ) |
| 988 | .optional()?) |
| 989 | } |
| 990 |
| 991 | pub fn ops(&self, workspace: &str, limit: i64) -> Result<Vec<(Op, bool)>> { |
| 992 | let mut stmt = |
| 993 | self.conn.prepare("SELECT id, at, kind, data, undone FROM ops WHERE workspace = ?1 ORDER BY id DESC LIMIT ?2")?; |
| 994 | let rows = stmt.query_map(params![workspace, limit], |row| Ok((Self::op_row(row)?, row.get::<_, i64>(4)? != 0)))?; |
| 995 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 996 | } |
| 997 |
| 998 | pub fn mark_undone(&self, id: i64) -> Result<()> { |
| 999 | self.conn.execute("UPDATE ops SET undone = 1 WHERE id = ?1", [id])?; |
| 1000 | Ok(()) |
| 1001 | } |
| 1002 |
| 1003 | // Mine files: what the user's own files looked like when last saved. |
| 1004 |
| 1005 | pub fn put_mine(&self, project: &str, path: &str, file: &Id, exec: bool) -> Result<()> { |
| 1006 | self.conn.execute( |
| 1007 | "INSERT OR REPLACE INTO mine (project, path, file, exec, at) VALUES (?1, ?2, ?3, ?4, ?5)", |
| 1008 | params![project, path, file.hex(), exec as i64, ids::now()], |
| 1009 | )?; |
| 1010 | Ok(()) |
| 1011 | } |
| 1012 |
| 1013 | pub fn mine(&self, project: &str) -> Result<Vec<(String, Id, bool)>> { |
| 1014 | let mut stmt = self.conn.prepare("SELECT path, file, exec FROM mine WHERE project = ?1 ORDER BY path")?; |
| 1015 | let rows = stmt.query_map([project], |r| Ok((r.get::<_, String>(0)?, id_col(r, 1)?, r.get::<_, i64>(2)? != 0)))?; |
| 1016 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 1017 | } |
| 1018 |
| 1019 | /// The newest version this machine has every version up to. A landing |
| 1020 | /// stores its own version before the ones others landed just ahead of |
| 1021 | /// it, so the newest version alone can leave a gap. |
| 1022 | pub fn versions_complete_to(&self, project: &str) -> Result<u64> { |
| 1023 | let start = self.history_from(project)?; |
| 1024 | let first: Option<i64> = |
| 1025 | self.conn.query_row("SELECT 1 FROM versions WHERE project = ?1 AND n = ?2", params![project, start as i64], |r| r.get(0)).optional()?; |
| 1026 | if first.is_none() { |
| 1027 | return Ok(start - 1); |
| 1028 | } |
| 1029 | let n: i64 = self.conn.query_row( |
| 1030 | "SELECT MIN(n) FROM versions v WHERE project = ?1 AND n >= ?2 AND NOT EXISTS (SELECT 1 FROM versions w WHERE w.project = v.project AND w.n = v.n + 1)", |
| 1031 | params![project, start as i64], |
| 1032 | |r| r.get(0), |
| 1033 | )?; |
| 1034 | Ok(n as u64) |
| 1035 | } |
| 1036 |
| 1037 | /// The first version of main this machine keeps (1 unless it started |
| 1038 | /// with recent history only). |
| 1039 | pub fn history_from(&self, project: &str) -> Result<u64> { |
| 1040 | let start: Option<i64> = self.conn.query_row("SELECT start FROM history WHERE project = ?1", [project], |r| r.get(0)).optional()?; |
| 1041 | Ok(start.map_or(1, |n| n.max(1) as u64)) |
| 1042 | } |
| 1043 |
| 1044 | pub fn set_history_from(&self, project: &str, n: u64) -> Result<()> { |
| 1045 | if n <= 1 { |
| 1046 | self.conn.execute("DELETE FROM history WHERE project = ?1", [project])?; |
| 1047 | } else { |
| 1048 | self.conn.execute("INSERT OR REPLACE INTO history (project, start) VALUES (?1, ?2)", params![project, n as i64])?; |
| 1049 | } |
| 1050 | Ok(()) |
| 1051 | } |
| 1052 |
| 1053 | /// Whether some version of main has this tree. |
| 1054 | pub fn versions_with_tree(&self, project: &str, tree: &Id) -> Result<bool> { |
| 1055 | Ok(self.conn.query_row("SELECT 1 FROM versions WHERE project = ?1 AND tree = ?2 LIMIT 1", params![project, tree.hex()], |_| Ok(())).optional()?.is_some()) |
| 1056 | } |
| 1057 |
| 1058 | /// A tree whose objects all reached the project's host. |
| 1059 | pub fn mark_uploaded_tree(&self, project: &str, tree: &Id) -> Result<()> { |
| 1060 | self.conn.execute("INSERT OR IGNORE INTO uploaded (project, tree) VALUES (?1, ?2)", params![project, tree.hex()])?; |
| 1061 | Ok(()) |
| 1062 | } |
| 1063 |
| 1064 | pub fn uploaded_tree(&self, project: &str, tree: &Id) -> Result<bool> { |
| 1065 | Ok(self.conn.query_row("SELECT 1 FROM uploaded WHERE project = ?1 AND tree = ?2", params![project, tree.hex()], |_| Ok(())).optional()?.is_some()) |
| 1066 | } |
| 1067 |
| 1068 | /// Where an imported project's git history came from. |
| 1069 | pub fn set_import(&self, project: &str, source: &str, branch: &str) -> Result<()> { |
| 1070 | self.conn.execute("INSERT OR REPLACE INTO imports (project, source, branch) VALUES (?1, ?2, ?3)", params![project, source, branch])?; |
| 1071 | Ok(()) |
| 1072 | } |
| 1073 |
| 1074 | pub fn import_source(&self, project: &str) -> Result<Option<(String, String)>> { |
| 1075 | Ok(self |
| 1076 | .conn |
| 1077 | .query_row("SELECT source, branch FROM imports WHERE project = ?1", [project], |r| Ok((r.get(0)?, r.get(1)?))) |
| 1078 | .optional()?) |
| 1079 | } |
| 1080 |
| 1081 | /// Native object → git object, for objects an import read from git. |
| 1082 | pub fn put_git_ids(&self, project: &str, ids: &[(Id, String)]) -> Result<()> { |
| 1083 | let tx = self.conn.unchecked_transaction()?; |
| 1084 | { |
| 1085 | let mut stmt = tx.prepare("INSERT OR REPLACE INTO git_ids (project, native, git) VALUES (?1, ?2, ?3)")?; |
| 1086 | for (native, git) in ids { |
| 1087 | stmt.execute(params![project, native.hex(), git])?; |
| 1088 | } |
| 1089 | } |
| 1090 | tx.commit()?; |
| 1091 | Ok(()) |
| 1092 | } |
| 1093 |
| 1094 | pub fn git_ids(&self, project: &str) -> Result<Vec<(String, String)>> { |
| 1095 | let mut stmt = self.conn.prepare("SELECT native, git FROM git_ids WHERE project = ?1")?; |
| 1096 | let rows = stmt.query_map([project], |r| Ok((r.get(0)?, r.get(1)?)))?; |
| 1097 | Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?) |
| 1098 | } |
| 1099 |
| 1100 | /// The newest version whose tree is `tree`. |
| 1101 | pub fn version_by_tree(&self, project: &str, tree: &Id) -> Result<Option<Version>> { |
| 1102 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 AND tree = ?2 ORDER BY n DESC LIMIT 1", Self::VERSION_COLUMNS); |
| 1103 | Ok(self.conn.query_row(&sql, params![project, tree.hex()], Self::version_row).optional()?) |
| 1104 | } |
| 1105 |
| 1106 | pub fn version_by_git(&self, project: &str, git: &str) -> Result<Option<Version>> { |
| 1107 | let sql = format!("SELECT {} FROM versions WHERE project = ?1 AND git = ?2 ORDER BY n DESC LIMIT 1", Self::VERSION_COLUMNS); |
| 1108 | Ok(self.conn.query_row(&sql, params![project, git], Self::version_row).optional()?) |
| 1109 | } |
| 1110 | } |
| 1111 |