123module history45import src.storage6import src.snapshot789pub struct Save {10pub mut:11 id string12 parent string13 message string14 created_at i6415 machine string16 is_checkpoint bool17 size_bytes i6418 agent string19 session string20 restores string21 replayed_from string22}2324const select_save = 'SELECT id, parent, message, created_at, machine, is_checkpoint, size_bytes,25 agent, session, restores, replayed_from FROM saves'2627fn read_save(stmt &storage.Stmt) Save {28 return Save{29 id: stmt.text(0)30 parent: stmt.text_or_none(1) or { '' }31 message: stmt.text(2)32 created_at: stmt.int(3)33 machine: stmt.text(4)34 is_checkpoint: stmt.int(5) != 035 size_bytes: stmt.int(6)36 agent: stmt.text_or_none(7) or { '' }37 session: stmt.text_or_none(8) or { '' }38 restores: stmt.text_or_none(9) or { '' }39 replayed_from: stmt.text_or_none(10) or { '' }40 }41}424344pub fn head(store &storage.Store) ?Save {45 id := store.state(storage.head_key) or { return none }46 return by_id(store, id) or { none }47}4849pub fn head_id(store &storage.Store) string {50 return store.state(storage.head_key) or { '' }51}5253pub fn by_id(store &storage.Store, id string) ?Save {54 mut stmt := store.db.prepare('${select_save} WHERE id = ?') or { return none }55 defer {56 stmt.finalize()57 }58 stmt.bind_text(1, id) or { return none }59 if stmt.step() or { return none } {60 return read_save(stmt)61 }62 return none63}64656667pub fn resolve(store &storage.Store, prefix string) !Save {68 if prefix.len < 6 {69 return error('"${prefix}" is too short — give at least 6 characters of a save ID')70 }71 mut stmt := store.db.prepare('${select_save} WHERE id LIKE ? ORDER BY id')!72 defer {73 stmt.finalize()74 }75 stmt.bind_text(1, '${prefix}%')!76 mut found := []Save{}77 for stmt.step()! {78 found << read_save(stmt)79 }80 match found.len {81 0 { return error('no save starts with "${prefix}"') }82 1 { return found[0] }83 else { return error('"${prefix}" matches ${found.len} saves — give more characters') }84 }85}86878889pub fn all_saves(store &storage.Store) ![]Save {90 mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, id DESC')!91 defer {92 stmt.finalize()93 }94 mut saves := []Save{}95 for stmt.step()! {96 saves << read_save(stmt)97 }98 return saves99}100101102pub fn line(store &storage.Store) ![]Save {103 mut saves := []Save{}104 mut cursor := head_id(store)105 for cursor != '' {106 save := by_id(store, cursor) or { break }107 saves << save108 cursor = save.parent109 }110 return saves111}112113114115116pub fn effective(store &storage.Store) !Save {117 current := head(store) or { return error('this repo has no saves yet') }118 if current.restores != '' {119 return by_id(store, current.restores) or {120 return error('save ${current.restores} is missing')121 }122 }123 return current124}125126127pub fn step_back(store &storage.Store, id string, n int) !Save {128 mut current := by_id(store, id) or { return error('save ${id} is missing') }129 for _ in 0 .. n {130 if current.parent == '' {131 return error('there is nothing before ${current.id} — it is the first save')132 }133 current = by_id(store, current.parent) or {134 return error('save ${current.parent} is missing')135 }136 }137 return current138}139140141pub fn session_start(store &storage.Store, id string) !Save {142 current := by_id(store, id) or { return error('save ${id} is missing') }143 if current.session == '' {144 return error('save ${current.id} is not part of a session')145 }146 mut oldest := current147 mut cursor := current.parent148 for cursor != '' {149 previous := by_id(store, cursor) or { break }150 if previous.session != current.session {151 break152 }153 oldest = previous154 cursor = previous.parent155 }156 return oldest157}158159160pub fn last_session(store &storage.Store) !string {161 for save in line(store)! {162 if save.session != '' {163 return save.session164 }165 }166 return error('no save on the line belongs to a session')167}168169170pub fn session_saves(store &storage.Store, name string) ![]Save {171 mut stmt := store.db.prepare('${select_save} WHERE session = ? ORDER BY created_at, id')!172 defer {173 stmt.finalize()174 }175 stmt.bind_text(1, name)!176 mut saves := []Save{}177 for stmt.step()! {178 saves << read_save(stmt)179 }180 return saves181}182183pub fn latest_checkpoint(store &storage.Store) ?Save {184 mut stmt := store.db.prepare('${select_save} WHERE is_checkpoint = 1 ORDER BY created_at DESC, id DESC LIMIT 1') or {185 return none186 }187 defer {188 stmt.finalize()189 }190 if stmt.step() or { return none } {191 return read_save(stmt)192 }193 return none194}195196pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {197 mut stmt :=198 store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!199 defer {200 stmt.finalize()201 }202 stmt.bind_text(1, save_id)!203 mut files := []snapshot.FileEntry{}204 for stmt.step()! {205 hash := stmt.text(1)206 files << snapshot.FileEntry{207 path: stmt.text(0)208 hash: hash209 size: store.object_size(hash) or { 0 }210 executable: stmt.int(2) != 0211 }212 }213 return files214}215216pub fn save_count(store &storage.Store) !i64 {217 return count(store, 'saves')218}219220pub fn checkpoint_count(store &storage.Store) !i64 {221 return count(store, 'saves WHERE is_checkpoint = 1')222}223224fn count(store &storage.Store, what string) !i64 {225 mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!226 defer {227 stmt.finalize()228 }229 if !stmt.step()! {230 return 0231 }232 return stmt.int(0)233}234235236237pub fn is_on_line(store &storage.Store, id string) bool {238 mut cursor := head_id(store)239 for cursor != '' {240 if cursor == id {241 return true242 }243 save := by_id(store, cursor) or { return false }244 cursor = save.parent245 }246 return false247}