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}112113114pub fn ancestry(store &storage.Store, id string) ![]Save {115 mut saves := []Save{}116 mut cursor := id117 for cursor != '' {118 save := by_id(store, cursor) or { break }119 saves << save120 cursor = save.parent121 }122 saves.reverse()123 return saves124}125126127128129pub fn effective(store &storage.Store) !Save {130 current := head(store) or { return error('this repo has no saves yet') }131 if current.restores != '' {132 return by_id(store, current.restores) or {133 return error('save ${current.restores} is missing')134 }135 }136 return current137}138139140pub fn step_back(store &storage.Store, id string, n int) !Save {141 mut current := by_id(store, id) or { return error('save ${id} is missing') }142 for _ in 0 .. n {143 if current.parent == '' {144 return error('there is nothing before ${current.id} — it is the first save')145 }146 current = by_id(store, current.parent) or {147 return error('save ${current.parent} is missing')148 }149 }150 return current151}152153154pub fn session_start(store &storage.Store, id string) !Save {155 current := by_id(store, id) or { return error('save ${id} is missing') }156 if current.session == '' {157 return error('save ${current.id} is not part of a session')158 }159 mut oldest := current160 mut cursor := current.parent161 for cursor != '' {162 previous := by_id(store, cursor) or { break }163 if previous.session != current.session {164 break165 }166 oldest = previous167 cursor = previous.parent168 }169 return oldest170}171172173pub fn last_session(store &storage.Store) !string {174 for save in line(store)! {175 if save.session != '' {176 return save.session177 }178 }179 return error('no save on the line belongs to a session')180}181182183pub fn session_saves(store &storage.Store, name string) ![]Save {184 mut stmt := store.db.prepare('${select_save} WHERE session = ? ORDER BY created_at, id')!185 defer {186 stmt.finalize()187 }188 stmt.bind_text(1, name)!189 mut saves := []Save{}190 for stmt.step()! {191 saves << read_save(stmt)192 }193 return saves194}195196pub fn latest_checkpoint(store &storage.Store) ?Save {197 mut stmt := store.db.prepare('${select_save} WHERE is_checkpoint = 1 ORDER BY created_at DESC, id DESC LIMIT 1') or {198 return none199 }200 defer {201 stmt.finalize()202 }203 if stmt.step() or { return none } {204 return read_save(stmt)205 }206 return none207}208209pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {210 mut stmt :=211 store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!212 defer {213 stmt.finalize()214 }215 stmt.bind_text(1, save_id)!216 mut files := []snapshot.FileEntry{}217 for stmt.step()! {218 hash := stmt.text(1)219 files << snapshot.FileEntry{220 path: stmt.text(0)221 hash: hash222 size: store.object_size(hash) or { 0 }223 executable: stmt.int(2) != 0224 }225 }226 return files227}228229pub fn save_count(store &storage.Store) !i64 {230 return count(store, 'saves')231}232233pub fn checkpoint_count(store &storage.Store) !i64 {234 return count(store, 'saves WHERE is_checkpoint = 1')235}236237fn count(store &storage.Store, what string) !i64 {238 mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!239 defer {240 stmt.finalize()241 }242 if !stmt.step()! {243 return 0244 }245 return stmt.int(0)246}247248249250pub fn is_on_line(store &storage.Store, id string) bool {251 mut cursor := head_id(store)252 for cursor != '' {253 if cursor == id {254 return true255 }256 save := by_id(store, cursor) or { return false }257 cursor = save.parent258 }259 return false260}