cmd_strip.v

4.5 KB · raw

1//! `koh strip` — remove a path from history, permanently.2//!3//! This is the only command that destroys information, so it is deliberately4//! loud: it needs a terminal, it warns about exactly what it will destroy, and5//! it asks twice — once the strict `y`, then the path typed out in full. Every6//! step is safe to repeat, so an interrupted strip finishes when it is re-run.7//!8//! The working tree is never touched. Afterwards the file is still on disk and9//! still untracked by the saves it was removed from, so the reminder to delete10//! or ignore it matters.1112module cli1314import json215import src.fmt16import src.history17import src.machine18import src.snapshot19import src.storage20import src.exchange2122pub fn cmd_strip(raw []string) int {23	args := parse('strip', raw, Options{24		values: ['save']25		flags:  ['json']26	}) or { return usage_error(err.msg()) }27	path := args.positional_at(0)28	if path == '' {29		return usage_error('strip needs a path, e.g. koh strip secrets.env')30	}31	// There is no --yes: a person has to be at the keyboard for this one.32	if !interactive() {33		return refuse(args.json(), 'strip needs a terminal — an agent can never strip')34	}35	store := open_store() or { return refuse(args.json(), err.msg()) }36	defer {37		store.close()38	}39	mut only_save := ''40	if given := args.value_or_none('save') {41		save := history.resolve(store, given) or { return refuse(args.json(), err.msg()) }42		only_save = save.id43	}44	scope := exchange.strip_scope(store, path, only_save) or { return refuse(args.json(), err.msg()) }45	mut local := exchange.LocalStrip{}46	if scope.files == 0 {47		// Nothing left here, but the remote half of an earlier strip may not48		// have finished; re-running is how it finishes.49		if !exchange.strip_pending(store, path) {50			ok('nothing in history matches ${path}')51			return 052		}53		note('${path} is already stripped here — finishing the remote')54	} else {55		where := if only_save == '' { 'every save' } else { 'save ${only_save}' }56		println('  ${fmt.red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information')57		println('  ${fmt.amber('!')} ${fmt.plural(scope.files, 'file')} in ${fmt.plural(scope.saves, 'save')}, ${fmt.plural(scope.objects, 'object')} — ${where}')58		println('  ${fmt.dim('strip removes the path from history; the file on disk is left alone')}')59		approved := confirm(false) or { return refuse(args.json(), err.msg()) }60		if !approved {61			note('nothing was stripped')62			return 163		}64		if !typed_confirm('type the path to confirm:', path) {65			note('nothing was stripped — the path did not match')66			return 167		}68		local = exchange.strip_local(store, path, only_save) or {69			return refuse(args.json(), err.msg())70		}71	}72	// A remote failure is reported, never fatal: the local strip has happened,73	// and running the same strip again finishes the remote.74	push := push_strip_to_remote(store, path)75	if args.json() {76		emit_json(json2.encode(StripJson{77			path:                   path78			save:                   only_save79			files_removed:          local.files_removed80			objects_dropped:        int(local.objects_dropped)81			records_rewritten:      push.pushed.records82			remote_objects_dropped: push.pushed.objects83			remote_failed:          push.failed84		}))85		return 086	}87	if scope.files > 0 {88		ok('stripped ${path} from ${fmt.plural(local.files_removed, 'file')} across ${fmt.plural(scope.saves, 'save')}')89		note('${local.objects_dropped} objects dropped')90	}91	if !push.skipped {92		if push.failed == '' {93			note('the remote was updated: ${push.pushed.records} save records rewritten, ${push.pushed.objects} objects dropped')94		} else {95			warn('the remote could not be updated: ${push.failed} — run the same strip again to finish it')96		}97	}98	note('remember to delete ${path} or add it to ${snapshot.ignore_file}')99	return 0100}101102struct PushOutcome {103mut:104	pushed  exchange.PushReport105	failed  string106	skipped bool107}108109// push_strip_to_remote tells this machine's remote about a strip.110fn push_strip_to_remote(store &storage.Store, path string) PushOutcome {111	if !machine.has_remote() {112		return PushOutcome{113			skipped: true114		}115	}116	mut backend := this_machine_backend() or {117		return PushOutcome{118			failed: err.msg()119		}120	}121	report := exchange.push_strip(store, mut backend, path) or {122		return PushOutcome{123			failed: err.msg()124		}125	}126	return PushOutcome{127		pushed: report128	}129}130131struct StripJson {132	path                   string133	save                   string134	files_removed          int135	objects_dropped        int136	records_rewritten      int137	remote_objects_dropped int138	remote_failed          string139}