cmd_strip.v

5.0 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 file itself is left on disk. A whole-history strip bans the path: it is9//! added to `.kohignore`, and no walk of the tree picks it up again.1011module cli1213import json214import src.fmt15import src.history16import src.machine17import src.snapshot18import src.storage19import src.exchange2021// cmd_strip needs a terminal and takes no --yes: a person confirms twice, and declining exits 1.22pub 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	sent := send_strip_to_remote(store, path)75	// A whole-history strip bans the path; the rule in `.kohignore` says so where a person reads,76	// and travels with the next save.77	ignored := only_save == '' && (snapshot.add_ignore(store.root, path) or {78		return refuse(args.json(), err.msg())79	})80	if args.json() {81		emit_json(json2.encode(StripJson{82			path:                   path83			save:                   only_save84			files_removed:          local.files_removed85			objects_dropped:        int(local.objects_dropped)86			records_rewritten:      sent.remote.records87			remote_objects_dropped: sent.remote.objects88			remote_failed:          sent.failed89		}))90		return 091	}92	if scope.files > 0 {93		ok('stripped ${path} from ${fmt.plural(local.files_removed, 'file')} across ${fmt.plural(scope.saves, 'save')}')94		note('${fmt.plural(int(local.objects_dropped), 'object')} dropped')95	}96	if !sent.skipped {97		if sent.failed == '' {98			note('the remote was updated: ${fmt.plural(sent.remote.records, 'save record')} rewritten, ${fmt.plural(sent.remote.objects, 'object')} dropped')99		} else {100			warn('the remote could not be updated: ${sent.failed} — run the same strip again to finish it')101		}102	}103	if only_save != '' {104		note('remember to delete ${path} or add it to ${snapshot.ignore_file}')105	} else if ignored {106		note('added /${path} to ${snapshot.ignore_file} — it can never be saved again')107	}108	return 0109}110111struct SendOutcome {112mut:113	remote  exchange.SendReport114	failed  string115	skipped bool116}117118// send_strip_to_remote tells this machine's remote about a strip.119fn send_strip_to_remote(store &storage.Store, path string) SendOutcome {120	if !machine.has_remote() {121		return SendOutcome{122			skipped: true123		}124	}125	mut backend := watched_backend('updating the remote') or {126		return SendOutcome{127			failed: err.msg()128		}129	}130	report := exchange.send_strip(store, mut backend, path) or {131		return SendOutcome{132			failed: err.msg()133		}134	}135	return SendOutcome{136		remote: report137	}138}139140struct StripJson {141	path                   string142	save                   string143	files_removed          int144	objects_dropped        int145	records_rewritten      int146	remote_objects_dropped int147	remote_failed          string148}