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}484950pub fn head_id(store &storage.Store) string {51 return store.state(storage.head_key) or { '' }52}5354pub fn by_id(store &storage.Store, id string) ?Save {55 mut stmt := store.db.prepare('${select_save} WHERE id = ?') or { return none }56 defer {57 stmt.finalize()58 }59 stmt.bind_text(1, id) or { return none }60 if stmt.step() or { return none } {61 return read_save(stmt)62 }63 return none64}65666768pub fn resolve(store &storage.Store, prefix string) !Save {69 if prefix.len < 6 {70 return error('"${prefix}" is too short — give at least 6 characters of a save ID')71 }72 mut stmt := store.db.prepare('${select_save} WHERE id LIKE ? ORDER BY id')!73 defer {74 stmt.finalize()75 }76 stmt.bind_text(1, '${prefix}%')!77 mut found := []Save{}78 for stmt.step()! {79 found << read_save(stmt)80 }81 match found.len {82 0 { return error('no save starts with "${prefix}"') }83 1 { return found[0] }84 else { return error('"${prefix}" matches ${found.len} saves — give more characters') }85 }86}87888990pub fn all_saves(store &storage.Store) ![]Save {91 92 93 mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, rowid DESC')!94 defer {95 stmt.finalize()96 }97 mut saves := []Save{}98 for stmt.step()! {99 saves << read_save(stmt)100 }101 return saves102}103104105pub fn line(store &storage.Store) ![]Save {106 mut saves := []Save{}107 mut cursor := head_id(store)108 for cursor != '' {109 save := by_id(store, cursor) or { break }110 saves << save111 cursor = save.parent112 }113 return saves114}115116117118119pub fn effective(store &storage.Store) !Save {120 current := head(store) or { return error('this repo has no saves yet') }121 if current.restores != '' {122 return by_id(store, current.restores) or {123 return error('save ${current.restores} is missing')124 }125 }126 return current127}128129130pub fn step_back(store &storage.Store, id string, n int) !Save {131 mut current := by_id(store, id) or { return error('save ${id} is missing') }132 for _ in 0 .. n {133 if current.parent == '' {134 return error('there is nothing before ${current.id} — it is the first save')135 }136 current = by_id(store, current.parent) or {137 return error('save ${current.parent} is missing')138 }139 }140 return current141}142143144pub fn session_start(store &storage.Store, id string) !Save {145 current := by_id(store, id) or { return error('save ${id} is missing') }146 if current.session == '' {147 return error('save ${current.id} is not part of a session')148 }149 mut oldest := current150 mut cursor := current.parent151 for cursor != '' {152 previous := by_id(store, cursor) or { break }153 if previous.session != current.session {154 break155 }156 oldest = previous157 cursor = previous.parent158 }159 return oldest160}161162163pub fn last_session(store &storage.Store) !string {164 for save in line(store)! {165 if save.session != '' {166 return save.session167 }168 }169 return error('no save on the line belongs to a session')170}171172173174pub fn session_saves(store &storage.Store, name string) ![]Save {175 mut saves := line(store)!.filter(it.session == name)176 saves.reverse_in_place()177 return saves178}179180181pub fn latest_checkpoint(store &storage.Store) ?Save {182 for save in line(store) or { return none } {183 if save.is_checkpoint {184 return save185 }186 }187 return none188}189190191pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {192 mut stmt :=193 store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!194 defer {195 stmt.finalize()196 }197 stmt.bind_text(1, save_id)!198 mut files := []snapshot.FileEntry{}199 for stmt.step()! {200 hash := stmt.text(1)201 files << snapshot.FileEntry{202 path: stmt.text(0)203 hash: hash204 size: store.object_size(hash) or { 0 }205 executable: stmt.int(2) != 0206 }207 }208 return files209}210211212pub fn save_count(store &storage.Store) !i64 {213 return count(store, 'saves')214}215216pub fn checkpoint_count(store &storage.Store) !i64 {217 return count(store, 'saves WHERE is_checkpoint = 1')218}219220fn count(store &storage.Store, what string) !i64 {221 mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!222 defer {223 stmt.finalize()224 }225 if !stmt.step()! {226 return 0227 }228 return stmt.int(0)229}