history_test.v

9.6 KB · raw

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	// Nothing changed, so a plain save refuses and a checkpoint records.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	// effective position follows restores, so undo keeps stepping back.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	// Walking back from the first save has nowhere to go; undo says so rather80	// than looping.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	// The tree at head is the restored one.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}124125// A combine leaves the replayed-away originals in history but off the line.126// `load --checkpoint` and `diff --session` answer about the line.127fn 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	// What a combine does to the line: head moves elsewhere, the save stays.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}150151// A save walks the tree before it takes the lock. If another command moved152// head meanwhile — a steal or an undo whose files are still being written — the153// walk is stale, and recording it on top of the new head would quietly undo154// that command. The tree is read again under the lock instead, and a restore155// still writing files is refused.156fn 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	// Another command lands a save and its files between that walk and the lock.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	// A restore that moved head and is still writing files: no save on top.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}202203// Two agents saving the same shared tree: the one that waited is told whose204// save already holds its changes, not just that nothing changed.205fn 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}231232// The save may read head just after a restore moved it, so its walk is not233// stale but the files are still landing: the mark alone decides.234fn 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	// Head read after the move, tree still the old one: not stale, but half.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}264265// A .kohignore change that lands with the other command must apply to the266// tree read again under the lock, or a newly ignored file would be saved.267fn 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}291292// Saves made within one second share a time; the all list must still show the293// newest first rather than ordering them by their hash.294fn 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}