history.v

6.1 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 the line's saves in a session, oldest first. A save170// replayed away by a combine is off the line, and its copy stands for it.171pub fn session_saves(store &storage.Store, name string) ![]Save {172	mut saves := line(store)!.filter(it.session == name)173	saves.reverse_in_place()174	return saves175}176177// latest_checkpoint is the newest checkpoint on the line.178pub fn latest_checkpoint(store &storage.Store) ?Save {179	for save in line(store) or { return none } {180		if save.is_checkpoint {181			return save182		}183	}184	return none185}186187pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {188	mut stmt :=189		store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!190	defer {191		stmt.finalize()192	}193	stmt.bind_text(1, save_id)!194	mut files := []snapshot.FileEntry{}195	for stmt.step()! {196		hash := stmt.text(1)197		files << snapshot.FileEntry{198			path:       stmt.text(0)199			hash:       hash200			size:       store.object_size(hash) or { 0 }201			executable: stmt.int(2) != 0202		}203	}204	return files205}206207pub fn save_count(store &storage.Store) !i64 {208	return count(store, 'saves')209}210211pub fn checkpoint_count(store &storage.Store) !i64 {212	return count(store, 'saves WHERE is_checkpoint = 1')213}214215fn count(store &storage.Store, what string) !i64 {216	mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!217	defer {218		stmt.finalize()219	}220	if !stmt.step()! {221		return 0222	}223	return stmt.int(0)224}