cmd_save.v

2.9 KB · raw

1//! `koh save` and `koh checkpoint`.23module cli45import json26import src.fmt7import src.history8import src.snapshot910// cmd_save records the tree as a save, or as a checkpoint. It needs a message, and a plain save refuses11// when nothing has changed; a checkpoint records an unchanged tree.12pub fn cmd_save(raw []string, is_checkpoint bool) int {13	verb := if is_checkpoint { 'checkpoint' } else { 'save' }14	args := parse(verb, raw, Options{15		values: ['agent', 'session']16		flags:  ['json']17	}) or { return usage_error(err.msg()) }18	message := args.positional_at(0)19	if message == '' {20		example := if is_checkpoint {21			'before combine replay engine'22		} else {23			'fix off-by-one in chunk boundary'24		}25		return usage_error('${verb} needs a message, e.g. koh ${verb} "${example}"')26	}27	store := open_store() or { return refuse(args.json(), err.msg()) }28	defer {29		store.close()30	}31	mut hashing := reading()32	walked := snapshot.walk_report(store, store.root, snapshot.load(store.root), mut hashing) or {33		return refuse(args.json(), err.msg())34	}35	save := history.save_worktree(store, store.root, snapshot.load(store.root), history.SaveInput{36		message:       message37		agent:         args.value('agent')38		session:       args.value('session')39		is_checkpoint: is_checkpoint40		storing:       fn (done int, total int) {41			if done % 100 == 1 || done == total {42				working('storing ${done} / ${total} files')43			}44		}45	}) or { return refuse(args.json(), err.msg()) }4647	files := history.files_of(store, save.id) or { []snapshot.FileEntry{} }48	if args.json() {49		emit_json(json2.encode(SaveOut{50			id:            save.id51			parent:        save.parent52			message:       save.message53			created_at:    save.created_at54			machine:       save.machine55			is_checkpoint: save.is_checkpoint56			agent:         save.agent57			session:       save.session58			files:         files.len59			size_bytes:    save.size_bytes60			unsafe_paths:  walked.unsafe_paths61		}))62		auto_offer(store, true)63		return 064	}65	for path in walked.unsafe_paths {66		warn('not saved: unsafe name ${path}')67	}68	ok('saved [${save.id}]  ${save.message}')69	mut detail := []string{}70	if save.is_checkpoint {71		detail << 'checkpoint'72	}73	detail << fmt.stamp(save.created_at)74	detail << fmt.plural(files.len, 'file')75	detail << fmt.human_bytes(save.size_bytes)76	note(detail.join(' · '))77	mut attribution := []string{}78	if save.agent != '' {79		attribution << 'agent: ${save.agent}'80	}81	if save.session != '' {82		attribution << 'session: ${save.session}'83	}84	if attribution.len > 0 {85		note(attribution.join(' · '))86	}87	auto_offer(store, false)88	return 089}9091// Every field is always present, so a consumer never has to tell "absent" from92// "missing key". An empty string means the save recorded none.93struct SaveOut {94	id            string95	parent        string96	message       string97	created_at    i6498	machine       string99	is_checkpoint bool100	agent         string101	session       string102	files         int103	size_bytes    i64104	unsafe_paths  []string105}