history.v

6.2 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	// Saves made within one second share a time; the order they were written91	// in breaks the tie, as in Koh 2. An ID is a hash and would shuffle them.92	mut stmt := store.db.prepare('${select_save} ORDER BY created_at DESC, rowid DESC')!93	defer {94		stmt.finalize()95	}96	mut saves := []Save{}97	for stmt.step()! {98		saves << read_save(stmt)99	}100	return saves101}102103// line returns the saves that are on the line, newest first.104pub fn line(store &storage.Store) ![]Save {105	mut saves := []Save{}106	mut cursor := head_id(store)107	for cursor != '' {108		save := by_id(store, cursor) or { break }109		saves << save110		cursor = save.parent111	}112	return saves113}114115// effective is where the line actually sits: `head.restores` when the newest116// save was a going-back, else `head`. Going back repeatedly therefore keeps117// stepping further back instead of undoing the undo.118pub fn effective(store &storage.Store) !Save {119	current := head(store) or { return error('this repo has no saves yet') }120	if current.restores != '' {121		return by_id(store, current.restores) or {122			return error('save ${current.restores} is missing')123		}124	}125	return current126}127128// step_back walks `n` parents back from `id`.129pub fn step_back(store &storage.Store, id string, n int) !Save {130	mut current := by_id(store, id) or { return error('save ${id} is missing') }131	for _ in 0 .. n {132		if current.parent == '' {133			return error('there is nothing before ${current.id} — it is the first save')134		}135		current = by_id(store, current.parent) or {136			return error('save ${current.parent} is missing')137		}138	}139	return current140}141142// session_start returns the oldest save of the session `id` belongs to.143pub fn session_start(store &storage.Store, id string) !Save {144	current := by_id(store, id) or { return error('save ${id} is missing') }145	if current.session == '' {146		return error('save ${current.id} is not part of a session')147	}148	mut oldest := current149	mut cursor := current.parent150	for cursor != '' {151		previous := by_id(store, cursor) or { break }152		if previous.session != current.session {153			break154		}155		oldest = previous156		cursor = previous.parent157	}158	return oldest159}160161// last_session finds the most recent session name on the line.162pub fn last_session(store &storage.Store) !string {163	for save in line(store)! {164		if save.session != '' {165			return save.session166		}167	}168	return error('no save on the line belongs to a session')169}170171// session_saves returns the line's saves in a session, oldest first. A save172// replayed away by a combine is off the line, and its copy stands for it.173pub fn session_saves(store &storage.Store, name string) ![]Save {174	mut saves := line(store)!.filter(it.session == name)175	saves.reverse_in_place()176	return saves177}178179// latest_checkpoint is the newest checkpoint on the line.180pub fn latest_checkpoint(store &storage.Store) ?Save {181	for save in line(store) or { return none } {182		if save.is_checkpoint {183			return save184		}185	}186	return none187}188189pub fn files_of(store &storage.Store, save_id string) ![]snapshot.FileEntry {190	mut stmt :=191		store.db.prepare('SELECT path, hash, executable FROM save_files WHERE save_id = ? ORDER BY path')!192	defer {193		stmt.finalize()194	}195	stmt.bind_text(1, save_id)!196	mut files := []snapshot.FileEntry{}197	for stmt.step()! {198		hash := stmt.text(1)199		files << snapshot.FileEntry{200			path:       stmt.text(0)201			hash:       hash202			size:       store.object_size(hash) or { 0 }203			executable: stmt.int(2) != 0204		}205	}206	return files207}208209pub fn save_count(store &storage.Store) !i64 {210	return count(store, 'saves')211}212213pub fn checkpoint_count(store &storage.Store) !i64 {214	return count(store, 'saves WHERE is_checkpoint = 1')215}216217fn count(store &storage.Store, what string) !i64 {218	mut stmt := store.db.prepare('SELECT COUNT(*) FROM ${what}')!219	defer {220		stmt.finalize()221	}222	if !stmt.step()! {223		return 0224	}225	return stmt.int(0)226}