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}2829pub const nothing_changed = 'nothing has changed since the last save'30313233pub fn commit_worktree(store &storage.Store, root string, ig snapshot.Ignore, input SaveInput) !Save {34 files := snapshot.walk(store, root, ig)!35 if input.restores == '' && !input.is_checkpoint {36 if previous := head(store) {37 if snapshot.same_tree(files, files_of(store, previous.id)!) {38 return error(nothing_changed)39 }40 }41 }42 return commit_files(store, root, files, input)!43}44454647pub fn commit_files(store &storage.Store, root string, files []snapshot.FileEntry, input SaveInput) !Save {48 store.begin()!49 save := record_save(store, root, files, input) or {50 store.rollback()51 return err52 }53 store.commit()!54 refresh_log_md(store)55 return save56}5758596061pub fn record_save(store &storage.Store, root string, files []snapshot.FileEntry, input SaveInput) !Save {62 if root != '' {63 snapshot.store_objects(store, root, files)!64 }65 return write_save(store, files, input)!66}6768fn write_save(store &storage.Store, files []snapshot.FileEntry, input SaveInput) !Save {69 created_at := if input.created_at > 0 { input.created_at } else { time.now().unix() }70 mut save := Save{71 parent: head_id(store)72 message: input.message73 created_at: created_at74 machine: if input.machine != '' { input.machine } else { machine.name() }75 is_checkpoint: input.is_checkpoint76 size_bytes: tree_size(store, files)77 agent: input.agent78 session: input.session79 restores: input.restores80 replayed_from: input.replayed_from81 }82 83 84 for {85 save.id = save_id(save, files)86 if by_id(store, save.id) == none {87 break88 }89 save.created_at++90 }91 insert_save(store, save, files)!92 store.set_state(storage.head_key, save.id)!93 return save94}9596979899pub fn import_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {100 insert_save(store, save, files)!101}102103104105106pub fn set_head(store &storage.Store, id string) ! {107 store.set_state(storage.head_key, id)!108}109110fn insert_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {111 mut stmt := store.db.prepare('INSERT INTO saves (id, parent, message, created_at, machine,112 is_checkpoint, size_bytes, agent, session, restores, replayed_from)113 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')!114 stmt.bind_text(1, save.id)!115 if save.parent == '' {116 stmt.bind_null(2)!117 } else {118 stmt.bind_text(2, save.parent)!119 }120 stmt.bind_text(3, save.message)!121 stmt.bind_int(4, save.created_at)!122 stmt.bind_text(5, save.machine)!123 stmt.bind_int(6, if save.is_checkpoint { 1 } else { 0 })!124 stmt.bind_int(7, save.size_bytes)!125 bind_optional(stmt, 8, save.agent)!126 bind_optional(stmt, 9, save.session)!127 bind_optional(stmt, 10, save.restores)!128 bind_optional(stmt, 11, save.replayed_from)!129 stmt.step()!130 stmt.finalize()131132 mut files_stmt :=133 store.db.prepare('INSERT INTO save_files (save_id, path, hash, executable) VALUES (?, ?, ?, ?)')!134 defer {135 files_stmt.finalize()136 }137 for file in files {138 files_stmt.bind_text(1, save.id)!139 files_stmt.bind_text(2, file.path)!140 files_stmt.bind_text(3, file.hash)!141 files_stmt.bind_int(4, if file.executable { 1 } else { 0 })!142 files_stmt.step()!143 144 files_stmt.reset()145 }146}147148fn bind_optional(stmt &storage.Stmt, index int, value string) ! {149 if value == '' {150 stmt.bind_null(index)!151 } else {152 stmt.bind_text(index, value)!153 }154}155156157158159pub fn save_id(save Save, files []snapshot.FileEntry) string {160 return storage.hash_hex(canonical(save, files).bytes())[..12]161}162163fn canonical(save Save, files []snapshot.FileEntry) string {164 mut sb := strings.Builder{}165 sb.write_string('koh3-save-v1\n')166 put_field(mut sb, 'parent', save.parent)167 put_field(mut sb, 'message', save.message)168 put_field(mut sb, 'created', save.created_at.str())169 put_field(mut sb, 'machine', save.machine)170 put_field(mut sb, 'checkpoint', if save.is_checkpoint { '1' } else { '0' })171 put_field(mut sb, 'agent', save.agent)172 put_field(mut sb, 'session', save.session)173 put_field(mut sb, 'files', files.len.str())174 for file in files {175 mark := if file.executable { 'x' } else { '-' }176 put_field(mut sb, 'file', '${mark}${file.hash} ${file.path}')177 }178 return sb.str()179}180181182183fn put_field(mut sb strings.Builder, name string, value string) {184 sb.write_string('${name}:${value.len}:')185 sb.write_string(value)186 sb.write_string('\n')187}188189fn tree_size(store &storage.Store, files []snapshot.FileEntry) i64 {190 mut total := i64(0)191 for file in files {192 total += store.object_size(file.hash) or { 0 }193 }194 return total195}