123456789module history1011import strings12import time13import src.machine14import src.snapshot15import src.storage1617pub struct SaveInput {18pub mut:19 message string20 agent string21 session string22 is_checkpoint bool23 restores string24 replayed_from string25 created_at i6426 machine string27 28 storing snapshot.Counted = unsafe { nil }29}3031pub const nothing_changed = 'nothing has changed since the last save'3233343536pub fn save_worktree(store &storage.Store, root string, ig snapshot.Ignore, input SaveInput) !Save {37 walked_head := head_id(store)38 files := snapshot.walk(store, root, ig)!39 store.begin()!40 save := save_walk(store, root, input, walked_head, files) or {41 store.rollback()42 return err43 }44 store.end()!45 refresh_log_md(store)46 return save47}4849505152535455fn save_walk(store &storage.Store, root string, input SaveInput, walked_head string, walked []snapshot.FileEntry) !Save {56 if snapshot.restore_pending(store) {57 return error(snapshot.restore_unfinished)58 }59 mut files := walked.clone()60 moved := head_id(store) != walked_head61 if moved {62 files = snapshot.walk(store, root, snapshot.load(root))!63 }64 if input.restores == '' && !input.is_checkpoint {65 if previous := head(store) {66 if snapshot.same_tree(files, files_of(store, previous.id)!) {67 if moved {68 69 70 return error('${nothing_changed} — [${previous.id}] "${previous.message}" was saved a moment ago and already holds this tree')71 }72 return error(nothing_changed)73 }74 }75 }76 return record_save(store, root, files, input)!77}78798081pub fn save_tree(store &storage.Store, root string, files []snapshot.FileEntry, input SaveInput) !Save {82 store.begin()!83 save := record_save(store, root, files, input) or {84 store.rollback()85 return err86 }87 store.end()!88 refresh_log_md(store)89 return save90}9192939495pub fn record_save(store &storage.Store, root string, files []snapshot.FileEntry, input SaveInput) !Save {96 if root != '' {97 snapshot.store_objects(store, root, files, input.storing)!98 }99 return write_save(store, files, input)!100}101102fn write_save(store &storage.Store, files []snapshot.FileEntry, input SaveInput) !Save {103 created_at := if input.created_at > 0 { input.created_at } else { time.now().unix() }104 mut save := Save{105 parent: head_id(store)106 message: input.message107 created_at: created_at108 machine: if input.machine != '' { input.machine } else { machine.name() }109 is_checkpoint: input.is_checkpoint110 size_bytes: tree_size(store, files)111 agent: input.agent112 session: input.session113 restores: input.restores114 replayed_from: input.replayed_from115 }116 117 118 for {119 save.id = save_id(save, files)120 if by_id(store, save.id) == none {121 break122 }123 save.created_at++124 }125 insert_save(store, save, files)!126 store.set_state(storage.head_key, save.id)!127 return save128}129130131132133pub fn import_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {134 insert_save(store, save, files)!135}136137138139140pub fn set_head(store &storage.Store, id string) ! {141 store.set_state(storage.head_key, id)!142}143144fn insert_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {145 mut stmt := store.db.prepare('INSERT INTO saves (id, parent, message, created_at, machine,146 is_checkpoint, size_bytes, agent, session, restores, replayed_from)147 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')!148 stmt.bind_text(1, save.id)!149 if save.parent == '' {150 stmt.bind_null(2)!151 } else {152 stmt.bind_text(2, save.parent)!153 }154 stmt.bind_text(3, save.message)!155 stmt.bind_int(4, save.created_at)!156 stmt.bind_text(5, save.machine)!157 stmt.bind_int(6, if save.is_checkpoint { 1 } else { 0 })!158 stmt.bind_int(7, save.size_bytes)!159 bind_optional(stmt, 8, save.agent)!160 bind_optional(stmt, 9, save.session)!161 bind_optional(stmt, 10, save.restores)!162 bind_optional(stmt, 11, save.replayed_from)!163 stmt.step()!164 stmt.finalize()165166 mut files_stmt :=167 store.db.prepare('INSERT INTO save_files (save_id, path, hash, executable) VALUES (?, ?, ?, ?)')!168 defer {169 files_stmt.finalize()170 }171 for file in files {172 files_stmt.bind_text(1, save.id)!173 files_stmt.bind_text(2, file.path)!174 files_stmt.bind_text(3, file.hash)!175 files_stmt.bind_int(4, if file.executable { 1 } else { 0 })!176 files_stmt.step()!177 178 files_stmt.reset()179 }180}181182fn bind_optional(stmt &storage.Stmt, index int, value string) ! {183 if value == '' {184 stmt.bind_null(index)!185 } else {186 stmt.bind_text(index, value)!187 }188}189190191192193pub fn save_id(save Save, files []snapshot.FileEntry) string {194 return storage.hash_hex(canonical(save, files).bytes())[..12]195}196197fn canonical(save Save, files []snapshot.FileEntry) string {198 mut sb := strings.Builder{}199 sb.write_string('koh3-save-v1\n')200 put_field(mut sb, 'parent', save.parent)201 put_field(mut sb, 'message', save.message)202 put_field(mut sb, 'created', save.created_at.str())203 put_field(mut sb, 'machine', save.machine)204 put_field(mut sb, 'checkpoint', if save.is_checkpoint { '1' } else { '0' })205 put_field(mut sb, 'agent', save.agent)206 put_field(mut sb, 'session', save.session)207 put_field(mut sb, 'files', files.len.str())208 for file in files {209 mark := if file.executable { 'x' } else { '-' }210 put_field(mut sb, 'file', '${mark}${file.hash} ${file.path}')211 }212 return sb.str()213}214215216217fn put_field(mut sb strings.Builder, name string, value string) {218 sb.write_string('${name}:${value.len}:')219 sb.write_string(value)220 sb.write_string('\n')221}222223fn tree_size(store &storage.Store, files []snapshot.FileEntry) i64 {224 mut total := i64(0)225 for file in files {226 total += store.object_size(file.hash) or { 0 }227 }228 return total229}