history.v

6.6 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// effective is where the line actually sits: `head.restores` when the newest114// save was a going-back, else `head`. Going back repeatedly therefore keeps115// stepping further back instead of undoing the undo.116pub fn effective(store &storage.Store) !Save {117	current := head(store) or { return error('this repo has no saves yet') }118	if current.restores != '' {119		return by_id(store, current.restores) or {120			return error('save ${current.restores} is missing')121		}122	}123	return current124}125126// step_back walks `n` parents back from `id`.127pub fn step_back(store &storage.Store, id string, n int) !Save {128	mut current := by_id(store, id) or { return error('save ${id} is missing') }129	for _ in 0 .. n {130		if current.parent == '' {131			return error('there is nothing before ${current.id} — it is the first save')132		}133		current = by_id(store, current.parent) or {134			return error('save ${current.parent} is missing')135		}136	}137	return current138}139140// session_start returns the oldest save of the session `id` belongs to.141pub fn session_start(store &storage.Store, id string) !Save {142	current := by_id(store, id) or { return error('save ${id} is missing') }143	if current.session == '' {144		return error('save ${current.id} is not part of a session')145	}146	mut oldest := current147	mut cursor := current.parent148	for cursor != '' {149		previous := by_id(store, cursor) or { break }150		if previous.session != current.session {151			break152		}153		oldest = previous154		cursor = previous.parent155	}156	return oldest157}158159// last_session finds the most recent session name on the line.160pub fn last_session(store &storage.Store) !string {161	for save in line(store)! {162		if save.session != '' {163			return save.session164		}165	}166	return error('no save on the line belongs to a session')167}168169// session_saves returns every save in a session, oldest first.170pub fn session_saves(store &storage.Store, name string) ![]Save {171	mut stmt := store.db.prepare('${select_save} WHERE session = ? ORDER BY created_at, id')!172	defer {173		stmt.finalize()174	}175	stmt.bind_text(1, name)!176	mut saves := []Save{}177	for stmt.step()! {178		saves << read_save(stmt)179	}180	return saves181}182183pub fn latest_checkpoint(store &storage.Store) ?Save {184	mut stmt := store.db.prepare('${select_save} WHERE is_checkpoint = 1 ORDER BY created_at DESC, id DESC LIMIT 1') or {185		return none186	}187	defer {188		stmt.finalize()189	}190	if stmt.step() or { return none } {191		return read_save(stmt)192	}193	return none194}195196pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {197	mut stmt :=198		store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!199	defer {200		stmt.finalize()201	}202	stmt.bind_text(1, save_id)!203	mut files := []snapshot.FileEntry{}204	for stmt.step()! {205		hash := stmt.text(1)206		files << snapshot.FileEntry{207			path:       stmt.text(0)208			hash:       hash209			size:       store.object_size(hash) or { 0 }210			executable: stmt.int(2) != 0211		}212	}213	return files214}215216pub fn save_count(store &storage.Store) !i64 {217	return count(store, 'saves')218}219220pub fn checkpoint_count(store &storage.Store) !i64 {221	return count(store, 'saves WHERE is_checkpoint = 1')222}223224fn count(store &storage.Store, what string) !i64 {225	mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!226	defer {227		stmt.finalize()228	}229	if !stmt.step()! {230		return 0231	}232	return stmt.int(0)233}234235// is_on_line reports whether `id` is reachable from head, so combine can tell236// replayed-away saves from live ones.237pub fn is_on_line(store &storage.Store, id string) bool {238	mut cursor := head_id(store)239	for cursor != '' {240		if cursor == id {241			return true242		}243		save := by_id(store, cursor) or { return false }244		cursor = save.parent245	}246	return false247}