history.v

6.5 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}4849// head_id is the head save's ID, or '' before the first save.50pub fn head_id(store &storage.Store) string {51	return store.state(storage.head_key) or { '' }52}5354pub fn by_id(store &storage.Store, id string) ?Save {55	mut stmt := store.db.prepare('${select_save} WHERE id = ?') or { return none }56	defer {57		stmt.finalize()58	}59	stmt.bind_text(1, id) or { return none }60	if stmt.step() or { return none } {61		return read_save(stmt)62	}63	return none64}6566// resolve accepts any unique prefix of six characters or more, the way Koh 267// did, so an agent can name a save without quoting the whole ID.68pub fn resolve(store &storage.Store, prefix string) !Save {69	if prefix.len < 6 {70		return error('"${prefix}" is too short — give at least 6 characters of a save ID')71	}72	mut stmt := store.db.prepare('${select_save} WHERE id LIKE ? ORDER BY id')!73	defer {74		stmt.finalize()75	}76	stmt.bind_text(1, '${prefix}%')!77	mut found := []Save{}78	for stmt.step()! {79		found << read_save(stmt)80	}81	match found.len {82		0 { return error('no save starts with "${prefix}"') }83		1 { return found[0] }84		else { return error('"${prefix}" matches ${found.len} saves — give more characters') }85	}86}8788// all_saves returns every save Koh knows, newest first. This is the "all" tab:89// it includes saves that were replayed away and are no longer on the line.90pub fn all_saves(store &storage.Store) ![]Save {91	// Saves made within one second share a time; the order they were written92	// in breaks the tie, as in Koh 2. An ID is a hash and would shuffle them.93	mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, rowid DESC')!94	defer {95		stmt.finalize()96	}97	mut saves := []Save{}98	for stmt.step()! {99		saves << read_save(stmt)100	}101	return saves102}103104// line returns the saves that are on the line, newest first.105pub fn line(store &storage.Store) ![]Save {106	mut saves := []Save{}107	mut cursor := head_id(store)108	for cursor != '' {109		save := by_id(store, cursor) or { break }110		saves << save111		cursor = save.parent112	}113	return saves114}115116// effective is where the line actually sits: `head.restores` when the newest117// save was a going-back, else `head`. Going back repeatedly therefore keeps118// stepping further back instead of undoing the undo.119pub fn effective(store &storage.Store) !Save {120	current := head(store) or { return error('this repo has no saves yet') }121	if current.restores != '' {122		return by_id(store, current.restores) or {123			return error('save ${current.restores} is missing')124		}125	}126	return current127}128129// step_back walks `n` parents back from `id`.130pub fn step_back(store &storage.Store, id string, n int) !Save {131	mut current := by_id(store, id) or { return error('save ${id} is missing') }132	for _ in 0 .. n {133		if current.parent == '' {134			return error('there is nothing before ${current.id} — it is the first save')135		}136		current = by_id(store, current.parent) or {137			return error('save ${current.parent} is missing')138		}139	}140	return current141}142143// session_start returns the oldest save of the session `id` belongs to.144pub fn session_start(store &storage.Store, id string) !Save {145	current := by_id(store, id) or { return error('save ${id} is missing') }146	if current.session == '' {147		return error('save ${current.id} is not part of a session')148	}149	mut oldest := current150	mut cursor := current.parent151	for cursor != '' {152		previous := by_id(store, cursor) or { break }153		if previous.session != current.session {154			break155		}156		oldest = previous157		cursor = previous.parent158	}159	return oldest160}161162// last_session finds the most recent session name on the line.163pub fn last_session(store &storage.Store) !string {164	for save in line(store)! {165		if save.session != '' {166			return save.session167		}168	}169	return error('no save on the line belongs to a session')170}171172// session_saves returns the line's saves in a session, oldest first. A save173// replayed away by a combine is off the line, and its copy stands for it.174pub fn session_saves(store &storage.Store, name string) ![]Save {175	mut saves := line(store)!.filter(it.session == name)176	saves.reverse_in_place()177	return saves178}179180// latest_checkpoint is the newest checkpoint on the line.181pub fn latest_checkpoint(store &storage.Store) ?Save {182	for save in line(store) or { return none } {183		if save.is_checkpoint {184			return save185		}186	}187	return none188}189190// files_of lists a save's files sorted by path, each with its object's size.191pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {192	mut stmt :=193		store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!194	defer {195		stmt.finalize()196	}197	stmt.bind_text(1, save_id)!198	mut files := []snapshot.FileEntry{}199	for stmt.step()! {200		hash := stmt.text(1)201		files << snapshot.FileEntry{202			path:       stmt.text(0)203			hash:       hash204			size:       store.object_size(hash) or { 0 }205			executable: stmt.int(2) != 0206		}207	}208	return files209}210211// save_count counts every save Koh holds, replayed-away ones included, unlike `line`.212pub fn save_count(store &storage.Store) !i64 {213	return count(store, 'saves')214}215216pub fn checkpoint_count(store &storage.Store) !i64 {217	return count(store, 'saves WHERE is_checkpoint = 1')218}219220fn count(store &storage.Store, what string) !i64 {221	mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!222	defer {223		stmt.finalize()224	}225	if !stmt.step()! {226		return 0227	}228	return stmt.int(0)229}