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}168169170171pub fn session_saves(store &storage.Store, name string) ![]Save {172 mut saves := line(store)!.filter(it.session == name)173 saves.reverse_in_place()174 return saves175}176177178pub fn latest_checkpoint(store &storage.Store) ?Save {179 for save in line(store) or { return none } {180 if save.is_checkpoint {181 return save182 }183 }184 return none185}186187pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {188 mut stmt :=189 store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!190 defer {191 stmt.finalize()192 }193 stmt.bind_text(1, save_id)!194 mut files := []snapshot.FileEntry{}195 for stmt.step()! {196 hash := stmt.text(1)197 files << snapshot.FileEntry{198 path: stmt.text(0)199 hash: hash200 size: store.object_size(hash) or { 0 }201 executable: stmt.int(2) != 0202 }203 }204 return files205}206207pub fn save_count(store &storage.Store) !i64 {208 return count(store, 'saves')209}210211pub fn checkpoint_count(store &storage.Store) !i64 {212 return count(store, 'saves WHERE is_checkpoint = 1')213}214215fn count(store &storage.Store, what string) !i64 {216 mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!217 defer {218 stmt.finalize()219 }220 if !stmt.step()! {221 return 0222 }223 return stmt.int(0)224}