1module history23import os4import src.snapshot5import src.storage67fn temp_repo(name string) !(storage.Store, string) {8 dir := os.join_path(os.temp_dir(), 'koh3_hist_${os.getpid()}_${name}')9 os.rmdir_all(dir) or {}10 os.mkdir_all(dir, os.MkdirParams{})!11 store := storage.create(dir)!12 return store, dir13}1415fn write_file(root string, rel string, text string) ! {16 full := os.join_path(root, rel)17 os.mkdir_all(os.dir(full), os.MkdirParams{})!18 os.write_file(full, text)!19}2021fn test_walk_then_save() {22 mut store, dir := temp_repo('save')!23 defer {24 store.close()25 os.rmdir_all(dir) or {}26 }27 write_file(dir, 'a.txt', 'hello\n')!28 write_file(dir, 'sub/b.txt', 'world\n')!29 entries := snapshot.walk(store, dir, snapshot.Ignore{})!30 assert entries.len == 23132 save := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{33 message: 'first save'34 })!35 assert save.id.len == 1236 assert save.parent == ''37 assert head_id(store) == save.id3839 files := files_of(store, save.id)!40 assert files.len == 241 assert files[0].path == 'a.txt'42 assert files[1].path == 'sub/b.txt'4344 45 if _ := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'again' }) {46 assert false, 'a save with no changes should refuse'47 } else {48 assert err.msg() == nothing_changed49 }50 second := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{51 message: 'checked anyway'52 is_checkpoint: true53 })!54 assert second.parent == save.id55 assert second.is_checkpoint56}5758fn test_going_back_is_recorded_not_rewound() {59 mut store, dir := temp_repo('back')!60 defer {61 store.close()62 os.rmdir_all(dir) or {}63 }64 write_file(dir, 'a.txt', 'one\n')!65 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'one' })!66 write_file(dir, 'a.txt', 'two\n')!67 second := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'two' })!6869 70 assert effective(store)!.id == second.id71 first_files := files_of(store, first.id)!72 restore := save_tree(store, '', first_files, SaveInput{73 message: 'undo: ${first.id}'74 restores: first.id75 })!76 assert restore.parent == second.id77 assert restore.restores == first.id78 assert effective(store)!.id == first.id79 80 81 if _ := step_back(store, effective(store)!.id, 1) {82 assert false, 'there is nothing before the first save'83 } else {84 assert err.msg().contains('nothing before')85 }8687 88 assert same_files(store, head_id(store), first_files)!89}9091fn same_files(store &storage.Store, id string, expected []snapshot.FileEntry) !bool {92 actual := files_of(store, id)!93 if actual.len != expected.len {94 return false95 }96 for i in 0 .. actual.len {97 if actual[i].path != expected[i].path || actual[i].hash != expected[i].hash {98 return false99 }100 }101 return true102}103104fn test_resolve_accepts_a_unique_prefix() {105 mut store, dir := temp_repo('resolve')!106 defer {107 store.close()108 os.rmdir_all(dir) or {}109 }110 write_file(dir, 'a.txt', 'one\n')!111 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{ message: 'one' })!112 assert resolve(store, first.id[..6])!.id == first.id113 if _ := resolve(store, 'abc') {114 assert false, 'a prefix shorter than six characters should be refused'115 } else {116 assert err.msg().contains('at least 6')117 }118 if _ := resolve(store, 'zzzzzz') {119 assert false, 'an unknown prefix should be refused'120 } else {121 assert err.msg().contains('no save starts with')122 }123}124125126127fn test_checkpoint_and_session_lookups_stay_on_the_line() {128 mut store, dir := temp_repo('on_line')!129 defer {130 store.close()131 os.rmdir_all(dir) or {}132 }133 write_file(dir, 'a.txt', 'one\n')!134 kept := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{135 message: 'on the line'136 session: 'work'137 is_checkpoint: true138 })!139 write_file(dir, 'a.txt', 'two\n')!140 save_worktree(store, dir, snapshot.Ignore{}, SaveInput{141 message: 'replayed away later'142 session: 'work'143 is_checkpoint: true144 })!145 146 set_head(store, kept.id)!147 assert latest_checkpoint(store)?.id == kept.id148 assert session_saves(store, 'work')!.map(it.id) == [kept.id]149}150151152153154155156fn test_a_save_whose_walk_went_stale_reads_the_tree_again() {157 mut store, dir := temp_repo('stale_walk')!158 defer {159 store.close()160 os.rmdir_all(dir) or {}161 }162 write_file(dir, 'a.txt', 'one\n')!163 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{164 message: 'first'165 })!166 write_file(dir, 'b.txt', 'mine\n')!167 stale := snapshot.walk(store, dir, snapshot.Ignore{})!168 169 write_file(dir, 'a.txt', 'two\n')!170 other := save_tree(store, dir,171 snapshot.walk(store, dir, snapshot.Ignore{})!.filter(it.path == 'a.txt'), SaveInput{172 message: 'somebody else'173 })!174 store.begin()!175 saved := save_walk(store, dir, SaveInput{176 message: 'mine'177 }, first.id, stale)!178 store.commit()!179 assert saved.parent == other.id180 a := files_of(store, saved.id)!.filter(it.path == 'a.txt')181 assert a.len == 1 && a[0].hash == storage.hash_hex('two\n'.bytes())182183 184 stale_again := snapshot.walk(store, dir, snapshot.Ignore{})!185 snapshot.mark_restore(store)!186 moved := save_tree(store, '', files_of(store, first.id)!, SaveInput{187 message: 'undo: ${first.id}'188 restores: first.id189 })!190 store.begin()!191 if _ := save_walk(store, dir, SaveInput{192 message: 'on a half tree'193 }, saved.id, stale_again)194 {195 assert false, 'a save must not land on a restore still writing files'196 } else {197 assert err.msg() == snapshot.restore_unfinished198 }199 store.rollback()200 assert head_id(store) == moved.id201}202203204205fn test_a_save_beaten_to_the_same_tree_names_the_save_that_holds_it() {206 mut store, dir := temp_repo('beaten')!207 defer {208 store.close()209 os.rmdir_all(dir) or {}210 }211 write_file(dir, 'a.txt', 'one\n')!212 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{213 message: 'first'214 })!215 write_file(dir, 'b.txt', 'both agents\n')!216 walked := snapshot.walk(store, dir, snapshot.Ignore{})!217 winner := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{218 message: 'agent one'219 })!220 store.begin()!221 if _ := save_walk(store, dir, SaveInput{222 message: 'agent two'223 }, first.id, walked)224 {225 assert false, 'the same tree must not be saved twice'226 } else {227 assert err.msg().contains('[${winner.id}] "agent one"')228 }229 store.rollback()230}231232233234fn test_a_save_after_a_restore_moved_head_waits_for_its_files() {235 mut store, dir := temp_repo('after_restore')!236 defer {237 store.close()238 os.rmdir_all(dir) or {}239 }240 write_file(dir, 'a.txt', 'one\n')!241 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{242 message: 'first'243 })!244 write_file(dir, 'a.txt', 'two\n')!245 save_worktree(store, dir, snapshot.Ignore{}, SaveInput{246 message: 'second'247 })!248 snapshot.mark_restore(store)!249 moved := save_tree(store, '', files_of(store, first.id)!, SaveInput{250 message: 'undo: ${first.id}'251 restores: first.id252 })!253 254 if _ := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{255 message: 'on a half tree'256 })257 {258 assert false, 'a save must not land while a restore writes its files'259 } else {260 assert err.msg() == snapshot.restore_unfinished261 }262 assert head_id(store) == moved.id263}264265266267fn test_a_stale_walk_is_read_again_with_the_current_ignore_rules() {268 mut store, dir := temp_repo('stale_rules')!269 defer {270 store.close()271 os.rmdir_all(dir) or {}272 }273 write_file(dir, 'a.txt', 'one\n')!274 first := save_worktree(store, dir, snapshot.Ignore{}, SaveInput{275 message: 'first'276 })!277 write_file(dir, 'secret.env', 'key\n')!278 stale := snapshot.walk(store, dir, snapshot.Ignore{})!279 write_file(dir, '.kohignore', 'secret.env\n')!280 save_tree(store, dir, snapshot.walk(store, dir, snapshot.load(dir))!, SaveInput{281 message: 'ignore the secret'282 })!283 write_file(dir, 'a.txt', 'two\n')!284 store.begin()!285 saved := save_walk(store, dir, SaveInput{286 message: 'mine'287 }, first.id, stale)!288 store.commit()!289 assert !files_of(store, saved.id)!.any(it.path == 'secret.env')290}291292293294fn test_all_saves_keeps_same_second_saves_newest_first() {295 mut store, dir := temp_repo('same_second')!296 defer {297 store.close()298 os.rmdir_all(dir) or {}299 }300 mut made := []string{}301 for i in 0 .. 6 {302 write_file(dir, 'a.txt', 'version ${i}\n')!303 made << save_worktree(store, dir, snapshot.Ignore{}, SaveInput{304 message: 'save ${i}'305 created_at: 1790000000306 })!.id307 }308 made.reverse_in_place()309 assert all_saves(store)!.map(it.id) == made310}