cmd_strip.v

4.9 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.exchange2021pub fn cmd_strip(raw []string) int {22	args := parse('strip', raw, Options{23		values: ['save']24		flags:  ['json']25	}) or { return usage_error(err.msg()) }26	path := args.positional_at(0)27	if path == '' {28		return usage_error('strip needs a path, e.g. koh strip secrets.env')29	}30	// There is no --yes: a person has to be at the keyboard for this one.31	if !interactive() {32		return refuse(args.json(), 'strip needs a terminal — an agent can never strip')33	}34	store := open_store() or { return refuse(args.json(), err.msg()) }35	defer {36		store.close()37	}38	mut only_save := ''39	if given := args.value_or_none('save') {40		save := history.resolve(store, given) or { return refuse(args.json(), err.msg()) }41		only_save = save.id42	}43	scope := exchange.strip_scope(store, path, only_save) or { return refuse(args.json(), err.msg()) }44	mut local := exchange.LocalStrip{}45	if scope.files == 0 {46		// Nothing left here, but the remote half of an earlier strip may not47		// have finished; re-running is how it finishes.48		if !exchange.strip_pending(store, path) {49			ok('nothing in history matches ${path}')50			return 051		}52		note('${path} is already stripped here — finishing the remote')53	} else {54		where := if only_save == '' { 'every save' } else { 'save ${only_save}' }55		println('  ${fmt.red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information')56		println('  ${fmt.amber('!')} ${fmt.plural(scope.files, 'file')} in ${fmt.plural(scope.saves, 'save')}, ${fmt.plural(scope.objects, 'object')} — ${where}')57		println('  ${fmt.dim('strip removes the path from history; the file on disk is left alone')}')58		approved := confirm(false) or { return refuse(args.json(), err.msg()) }59		if !approved {60			note('nothing was stripped')61			return 162		}63		if !typed_confirm('type the path to confirm:', path) {64			note('nothing was stripped — the path did not match')65			return 166		}67		local = exchange.strip_local(store, path, only_save) or {68			return refuse(args.json(), err.msg())69		}70	}71	// A remote failure is reported, never fatal: the local strip has happened,72	// and running the same strip again finishes the remote.73	sent := send_strip_to_remote(store, path)74	// A whole-history strip bans the path; the rule in `.kohignore` says so where a person reads,75	// and travels with the next save.76	ignored := only_save == '' && (snapshot.add_ignore(store.root, path) or {77		return refuse(args.json(), err.msg())78	})79	if args.json() {80		emit_json(json2.encode(StripJson{81			path:                   path82			save:                   only_save83			files_removed:          local.files_removed84			objects_dropped:        int(local.objects_dropped)85			records_rewritten:      sent.remote.records86			remote_objects_dropped: sent.remote.objects87			remote_failed:          sent.failed88		}))89		return 090	}91	if scope.files > 0 {92		ok('stripped ${path} from ${fmt.plural(local.files_removed, 'file')} across ${fmt.plural(scope.saves, 'save')}')93		note('${local.objects_dropped} objects dropped')94	}95	if !sent.skipped {96		if sent.failed == '' {97			note('the remote was updated: ${sent.remote.records} save records rewritten, ${sent.remote.objects} objects dropped')98		} else {99			warn('the remote could not be updated: ${sent.failed} — run the same strip again to finish it')100		}101	}102	if only_save != '' {103		note('remember to delete ${path} or add it to ${snapshot.ignore_file}')104	} else if ignored {105		note('added /${path} to ${snapshot.ignore_file} — it can never be saved again')106	}107	return 0108}109110struct SendOutcome {111mut:112	remote  exchange.SendReport113	failed  string114	skipped bool115}116117// send_strip_to_remote tells this machine's remote about a strip.118fn send_strip_to_remote(store &storage.Store, path string) SendOutcome {119	if !machine.has_remote() {120		return SendOutcome{121			skipped: true122		}123	}124	mut backend := this_machine_backend() or {125		return SendOutcome{126			failed: err.msg()127		}128	}129	report := exchange.send_strip(store, mut backend, path) or {130		return SendOutcome{131			failed: err.msg()132		}133	}134	return SendOutcome{135		remote: report136	}137}138139struct StripJson {140	path                   string141	save                   string142	files_removed          int143	objects_dropped        int144	records_rewritten      int145	remote_objects_dropped int146	remote_failed          string147}