//! History: one line of saves, and the queries every command needs. module history import src.storage import src.snapshot // A save is a full snapshot of the tree plus what made it. pub struct Save { pub mut: id string parent string message string created_at i64 machine string is_checkpoint bool size_bytes i64 agent string session string restores string replayed_from string } const select_save = 'SELECT id, parent, message, created_at, machine, is_checkpoint, size_bytes, agent, session, restores, replayed_from FROM saves' fn read_save(stmt &storage.Stmt) Save { return Save{ id: stmt.text(0) parent: stmt.text_or_none(1) or { '' } message: stmt.text(2) created_at: stmt.int(3) machine: stmt.text(4) is_checkpoint: stmt.int(5) != 0 size_bytes: stmt.int(6) agent: stmt.text_or_none(7) or { '' } session: stmt.text_or_none(8) or { '' } restores: stmt.text_or_none(9) or { '' } replayed_from: stmt.text_or_none(10) or { '' } } } // head is the newest save on the line. pub fn head(store &storage.Store) ?Save { id := store.state(storage.head_key) or { return none } return by_id(store, id) or { none } } // head_id is the head save's ID, or '' before the first save. pub fn head_id(store &storage.Store) string { return store.state(storage.head_key) or { '' } } pub fn by_id(store &storage.Store, id string) ?Save { mut stmt := store.db.prepare('${select_save} WHERE id = ?') or { return none } defer { stmt.finalize() } stmt.bind_text(1, id) or { return none } if stmt.step() or { return none } { return read_save(stmt) } return none } // resolve accepts any unique prefix of six characters or more, the way Koh 2 // did, so an agent can name a save without quoting the whole ID. pub fn resolve(store &storage.Store, prefix string) !Save { if prefix.len < 6 { return error('"${prefix}" is too short — give at least 6 characters of a save ID') } mut stmt := store.db.prepare('${select_save} WHERE id LIKE ? ORDER BY id')! defer { stmt.finalize() } stmt.bind_text(1, '${prefix}%')! mut found := []Save{} for stmt.step()! { found << read_save(stmt) } match found.len { 0 { return error('no save starts with "${prefix}"') } 1 { return found[0] } else { return error('"${prefix}" matches ${found.len} saves — give more characters') } } } // all_saves returns every save Koh knows, newest first. This is the "all" tab: // it includes saves that were replayed away and are no longer on the line. pub fn all_saves(store &storage.Store) ![]Save { // Saves made within one second share a time; the order they were written // in breaks the tie, as in Koh 2. An ID is a hash and would shuffle them. mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, rowid DESC')! defer { stmt.finalize() } mut saves := []Save{} for stmt.step()! { saves << read_save(stmt) } return saves } // line returns the saves that are on the line, newest first. pub fn line(store &storage.Store) ![]Save { mut saves := []Save{} mut cursor := head_id(store) for cursor != '' { save := by_id(store, cursor) or { break } saves << save cursor = save.parent } return saves } // effective is where the line actually sits: `head.restores` when the newest // save was a going-back, else `head`. Going back repeatedly therefore keeps // stepping further back instead of undoing the undo. pub fn effective(store &storage.Store) !Save { current := head(store) or { return error('this repo has no saves yet') } if current.restores != '' { return by_id(store, current.restores) or { return error('save ${current.restores} is missing') } } return current } // step_back walks `n` parents back from `id`. pub fn step_back(store &storage.Store, id string, n int) !Save { mut current := by_id(store, id) or { return error('save ${id} is missing') } for _ in 0 .. n { if current.parent == '' { return error('there is nothing before ${current.id} — it is the first save') } current = by_id(store, current.parent) or { return error('save ${current.parent} is missing') } } return current } // session_start returns the oldest save of the session `id` belongs to. pub fn session_start(store &storage.Store, id string) !Save { current := by_id(store, id) or { return error('save ${id} is missing') } if current.session == '' { return error('save ${current.id} is not part of a session') } mut oldest := current mut cursor := current.parent for cursor != '' { previous := by_id(store, cursor) or { break } if previous.session != current.session { break } oldest = previous cursor = previous.parent } return oldest } // last_session finds the most recent session name on the line. pub fn last_session(store &storage.Store) !string { for save in line(store)! { if save.session != '' { return save.session } } return error('no save on the line belongs to a session') } // session_saves returns the line's saves in a session, oldest first. A save // replayed away by a combine is off the line, and its copy stands for it. pub fn session_saves(store &storage.Store, name string) ![]Save { mut saves := line(store)!.filter(it.session == name) saves.reverse_in_place() return saves } // latest_checkpoint is the newest checkpoint on the line. pub fn latest_checkpoint(store &storage.Store) ?Save { for save in line(store) or { return none } { if save.is_checkpoint { return save } } return none } // files_of lists a save's files sorted by path, each with its object's size. pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry { mut stmt := store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')! defer { stmt.finalize() } stmt.bind_text(1, save_id)! mut files := []snapshot.FileEntry{} for stmt.step()! { hash := stmt.text(1) files << snapshot.FileEntry{ path: stmt.text(0) hash: hash size: store.object_size(hash) or { 0 } executable: stmt.int(2) != 0 } } return files } // save_count counts every save Koh holds, replayed-away ones included, unlike `line`. pub fn save_count(store &storage.Store) !i64 { return count(store, 'saves') } pub fn checkpoint_count(store &storage.Store) !i64 { return count(store, 'saves WHERE is_checkpoint = 1') } fn count(store &storage.Store, what string) !i64 { mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')! defer { stmt.finalize() } if !stmt.step()! { return 0 } return stmt.int(0) }