history.v

6.9 KB · raw

1//! History: one line of saves, and the queries every command needs.23module history45import src.storage6import src.snapshot78// A save is a full snapshot of the tree plus what made it.9pub struct Save {10pub mut:11	id            string12	parent        string13	message       string14	created_at    i6415	machine       string16	is_checkpoint bool17	size_bytes    i6418	agent         string19	session       string20	restores      string21	replayed_from string22}2324const select_save = 'SELECT id, parent, message, created_at, machine, is_checkpoint, size_bytes,25	agent, session, restores, replayed_from FROM saves'2627fn read_save(stmt &storage.Stmt) Save {28	return Save{29		id:            stmt.text(0)30		parent:        stmt.text_or_none(1) or { '' }31		message:       stmt.text(2)32		created_at:    stmt.int(3)33		machine:       stmt.text(4)34		is_checkpoint: stmt.int(5) != 035		size_bytes:    stmt.int(6)36		agent:         stmt.text_or_none(7) or { '' }37		session:       stmt.text_or_none(8) or { '' }38		restores:      stmt.text_or_none(9) or { '' }39		replayed_from: stmt.text_or_none(10) or { '' }40	}41}4243// head is the newest save on the line.44pub fn head(store &storage.Store) ?Save {45	id := store.state(storage.head_key) or { return none }46	return by_id(store, id) or { none }47}4849pub fn head_id(store &storage.Store) string {50	return store.state(storage.head_key) or { '' }51}5253pub fn by_id(store &storage.Store, id string) ?Save {54	mut stmt := store.db.prepare('${select_save} WHERE id = ?') or { return none }55	defer {56		stmt.finalize()57	}58	stmt.bind_text(1, id) or { return none }59	if stmt.step() or { return none } {60		return read_save(stmt)61	}62	return none63}6465// resolve accepts any unique prefix of six characters or more, the way Koh 266// did, so an agent can name a save without quoting the whole ID.67pub fn resolve(store &storage.Store, prefix string) !Save {68	if prefix.len < 6 {69		return error('"${prefix}" is too short — give at least 6 characters of a save ID')70	}71	mut stmt := store.db.prepare('${select_save} WHERE id LIKE ? ORDER BY id')!72	defer {73		stmt.finalize()74	}75	stmt.bind_text(1, '${prefix}%')!76	mut found := []Save{}77	for stmt.step()! {78		found << read_save(stmt)79	}80	match found.len {81		0 { return error('no save starts with "${prefix}"') }82		1 { return found[0] }83		else { return error('"${prefix}" matches ${found.len} saves — give more characters') }84	}85}8687// all_saves returns every save Koh knows, newest first. This is the "all" tab:88// it includes saves that were replayed away and are no longer on the line.89pub fn all_saves(store &storage.Store) ![]Save {90	mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, id DESC')!91	defer {92		stmt.finalize()93	}94	mut saves := []Save{}95	for stmt.step()! {96		saves << read_save(stmt)97	}98	return saves99}100101// line returns the saves that are on the line, newest first.102pub fn line(store &storage.Store) ![]Save {103	mut saves := []Save{}104	mut cursor := head_id(store)105	for cursor != '' {106		save := by_id(store, cursor) or { break }107		saves << save108		cursor = save.parent109	}110	return saves111}112113// ancestry returns the saves behind `id`, oldest first.114pub fn ancestry(store &storage.Store, id string) ![]Save {115	mut saves := []Save{}116	mut cursor := id117	for cursor != '' {118		save := by_id(store, cursor) or { break }119		saves << save120		cursor = save.parent121	}122	saves.reverse()123	return saves124}125126// effective is where the line actually sits: `head.restores` when the newest127// save was a going-back, else `head`. Going back repeatedly therefore keeps128// stepping further back instead of undoing the undo.129pub fn effective(store &storage.Store) !Save {130	current := head(store) or { return error('this repo has no saves yet') }131	if current.restores != '' {132		return by_id(store, current.restores) or {133			return error('save ${current.restores} is missing')134		}135	}136	return current137}138139// step_back walks `n` parents back from `id`.140pub fn step_back(store &storage.Store, id string, n int) !Save {141	mut current := by_id(store, id) or { return error('save ${id} is missing') }142	for _ in 0 .. n {143		if current.parent == '' {144			return error('there is nothing before ${current.id} — it is the first save')145		}146		current = by_id(store, current.parent) or {147			return error('save ${current.parent} is missing')148		}149	}150	return current151}152153// session_start returns the oldest save of the session `id` belongs to.154pub fn session_start(store &storage.Store, id string) !Save {155	current := by_id(store, id) or { return error('save ${id} is missing') }156	if current.session == '' {157		return error('save ${current.id} is not part of a session')158	}159	mut oldest := current160	mut cursor := current.parent161	for cursor != '' {162		previous := by_id(store, cursor) or { break }163		if previous.session != current.session {164			break165		}166		oldest = previous167		cursor = previous.parent168	}169	return oldest170}171172// last_session finds the most recent session name on the line.173pub fn last_session(store &storage.Store) !string {174	for save in line(store)! {175		if save.session != '' {176			return save.session177		}178	}179	return error('no save on the line belongs to a session')180}181182// session_saves returns every save in a session, oldest first.183pub fn session_saves(store &storage.Store, name string) ![]Save {184	mut stmt := store.db.prepare('${select_save} WHERE session = ? ORDER BY created_at, id')!185	defer {186		stmt.finalize()187	}188	stmt.bind_text(1, name)!189	mut saves := []Save{}190	for stmt.step()! {191		saves << read_save(stmt)192	}193	return saves194}195196pub fn latest_checkpoint(store &storage.Store) ?Save {197	mut stmt := store.db.prepare('${select_save} WHERE is_checkpoint = 1 ORDER BY created_at DESC, id DESC LIMIT 1') or {198		return none199	}200	defer {201		stmt.finalize()202	}203	if stmt.step() or { return none } {204		return read_save(stmt)205	}206	return none207}208209pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {210	mut stmt :=211		store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!212	defer {213		stmt.finalize()214	}215	stmt.bind_text(1, save_id)!216	mut files := []snapshot.FileEntry{}217	for stmt.step()! {218		hash := stmt.text(1)219		files << snapshot.FileEntry{220			path:       stmt.text(0)221			hash:       hash222			size:       store.object_size(hash) or { 0 }223			executable: stmt.int(2) != 0224		}225	}226	return files227}228229pub fn save_count(store &storage.Store) !i64 {230	return count(store, 'saves')231}232233pub fn checkpoint_count(store &storage.Store) !i64 {234	return count(store, 'saves WHERE is_checkpoint = 1')235}236237fn count(store &storage.Store, what string) !i64 {238	mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!239	defer {240		stmt.finalize()241	}242	if !stmt.step()! {243		return 0244	}245	return stmt.int(0)246}247248// is_on_line reports whether `id` is reachable from head, so combine can tell249// replayed-away saves from live ones.250pub fn is_on_line(store &storage.Store, id string) bool {251	mut cursor := head_id(store)252	for cursor != '' {253		if cursor == id {254			return true255		}256		save := by_id(store, cursor) or { return false }257		cursor = save.parent258	}259	return false260}