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}101102103104fn write_save(store &storage.Store, files []snapshot.FileEntry, input SaveInput) !Save {105 created_at := if input.created_at > 0 { input.created_at } else { time.now().unix() }106 mut save := Save{107 parent: head_id(store)108 message: input.message109 created_at: created_at110 machine: if input.machine != '' { input.machine } else { machine.name() }111 is_checkpoint: input.is_checkpoint112 size_bytes: tree_size(store, files)113 agent: input.agent114 session: input.session115 restores: input.restores116 replayed_from: input.replayed_from117 }118 119 120 for {121 save.id = save_id(save, files)122 if by_id(store, save.id) == none {123 break124 }125 save.created_at++126 }127 insert_save(store, save, files)!128 store.set_state(storage.head_key, save.id)!129 return save130}131132133134135pub fn import_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {136 insert_save(store, save, files)!137}138139140141142pub fn set_head(store &storage.Store, id string) ! {143 store.set_state(storage.head_key, id)!144}145146147fn insert_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {148 mut stmt := store.db.prepare('INSERT INTO saves (id, parent, message, created_at, machine,149 is_checkpoint, size_bytes, agent, session, restores, replayed_from)150 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')!151 stmt.bind_text(1, save.id)!152 if save.parent == '' {153 stmt.bind_null(2)!154 } else {155 stmt.bind_text(2, save.parent)!156 }157 stmt.bind_text(3, save.message)!158 stmt.bind_int(4, save.created_at)!159 stmt.bind_text(5, save.machine)!160 stmt.bind_int(6, if save.is_checkpoint { 1 } else { 0 })!161 stmt.bind_int(7, save.size_bytes)!162 bind_optional(stmt, 8, save.agent)!163 bind_optional(stmt, 9, save.session)!164 bind_optional(stmt, 10, save.restores)!165 bind_optional(stmt, 11, save.replayed_from)!166 stmt.step()!167 stmt.finalize()168169 mut files_stmt :=170 store.db.prepare('INSERT INTO save_files (save_id, path, hash, executable) VALUES (?, ?, ?, ?)')!171 defer {172 files_stmt.finalize()173 }174 for file in files {175 files_stmt.bind_text(1, save.id)!176 files_stmt.bind_text(2, file.path)!177 files_stmt.bind_text(3, file.hash)!178 files_stmt.bind_int(4, if file.executable { 1 } else { 0 })!179 files_stmt.step()!180 181 files_stmt.reset()182 }183}184185fn bind_optional(stmt &storage.Stmt, index int, value string) ! {186 if value == '' {187 stmt.bind_null(index)!188 } else {189 stmt.bind_text(index, value)!190 }191}192193194195196pub fn save_id(save Save, files []snapshot.FileEntry) string {197 return storage.hash_hex(canonical(save, files).bytes())[..12]198}199200fn canonical(save Save, files []snapshot.FileEntry) string {201 mut sb := strings.Builder{}202 sb.write_string('koh3-save-v1\n')203 put_field(mut sb, 'parent', save.parent)204 put_field(mut sb, 'message', save.message)205 put_field(mut sb, 'created', save.created_at.str())206 put_field(mut sb, 'machine', save.machine)207 put_field(mut sb, 'checkpoint', if save.is_checkpoint { '1' } else { '0' })208 put_field(mut sb, 'agent', save.agent)209 put_field(mut sb, 'session', save.session)210 put_field(mut sb, 'files', files.len.str())211 for file in files {212 mark := if file.executable { 'x' } else { '-' }213 put_field(mut sb, 'file', '${mark}${file.hash} ${file.path}')214 }215 return sb.str()216}217218219220fn put_field(mut sb strings.Builder, name string, value string) {221 sb.write_string('${name}:${value.len}:')222 sb.write_string(value)223 sb.write_string('\n')224}225226fn tree_size(store &storage.Store, files []snapshot.FileEntry) i64 {227 mut total := i64(0)228 for file in files {229 total += store.object_size(file.hash) or { 0 }230 }231 return total232}