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 91 92 mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, rowid DESC')!93 defer {94 stmt.finalize()95 }96 mut saves := []Save{}97 for stmt.step()! {98 saves << read_save(stmt)99 }100 return saves101}102103104pub fn line(store &storage.Store) ![]Save {105 mut saves := []Save{}106 mut cursor := head_id(store)107 for cursor != '' {108 save := by_id(store, cursor) or { break }109 saves << save110 cursor = save.parent111 }112 return saves113}114115116117118pub fn effective(store &storage.Store) !Save {119 current := head(store) or { return error('this repo has no saves yet') }120 if current.restores != '' {121 return by_id(store, current.restores) or {122 return error('save ${current.restores} is missing')123 }124 }125 return current126}127128129pub fn step_back(store &storage.Store, id string, n int) !Save {130 mut current := by_id(store, id) or { return error('save ${id} is missing') }131 for _ in 0 .. n {132 if current.parent == '' {133 return error('there is nothing before ${current.id} — it is the first save')134 }135 current = by_id(store, current.parent) or {136 return error('save ${current.parent} is missing')137 }138 }139 return current140}141142143pub fn session_start(store &storage.Store, id string) !Save {144 current := by_id(store, id) or { return error('save ${id} is missing') }145 if current.session == '' {146 return error('save ${current.id} is not part of a session')147 }148 mut oldest := current149 mut cursor := current.parent150 for cursor != '' {151 previous := by_id(store, cursor) or { break }152 if previous.session != current.session {153 break154 }155 oldest = previous156 cursor = previous.parent157 }158 return oldest159}160161162pub fn last_session(store &storage.Store) !string {163 for save in line(store)! {164 if save.session != '' {165 return save.session166 }167 }168 return error('no save on the line belongs to a session')169}170171172173pub fn session_saves(store &storage.Store, name string) ![]Save {174 mut saves := line(store)!.filter(it.session == name)175 saves.reverse_in_place()176 return saves177}178179180pub fn latest_checkpoint(store &storage.Store) ?Save {181 for save in line(store) or { return none } {182 if save.is_checkpoint {183 return save184 }185 }186 return none187}188189pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {190 mut stmt :=191 store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!192 defer {193 stmt.finalize()194 }195 stmt.bind_text(1, save_id)!196 mut files := []snapshot.FileEntry{}197 for stmt.step()! {198 hash := stmt.text(1)199 files << snapshot.FileEntry{200 path: stmt.text(0)201 hash: hash202 size: store.object_size(hash) or { 0 }203 executable: stmt.int(2) != 0204 }205 }206 return files207}208209pub fn save_count(store &storage.Store) !i64 {210 return count(store, 'saves')211}212213pub fn checkpoint_count(store &storage.Store) !i64 {214 return count(store, 'saves WHERE is_checkpoint = 1')215}216217fn count(store &storage.Store, what string) !i64 {218 mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!219 defer {220 stmt.finalize()221 }222 if !stmt.step()! {223 return 0224 }225 return stmt.int(0)226}