//! `koh strip` — remove a path from history, permanently. //! //! This is the only command that destroys information, so it is deliberately //! loud: it needs a terminal, it warns about exactly what it will destroy, and //! it asks twice — once the strict `y`, then the path typed out in full. Every //! step is safe to repeat, so an interrupted strip finishes when it is re-run. //! //! The file itself is left on disk. A whole-history strip bans the path: it is //! added to `.kohignore`, and no walk of the tree picks it up again. module cli import json2 import src.fmt import src.history import src.machine import src.snapshot import src.storage import src.exchange // cmd_strip needs a terminal and takes no --yes: a person confirms twice, and declining exits 1. pub fn cmd_strip(raw []string) int { args := parse('strip', raw, Options{ values: ['save'] flags: ['json'] }) or { return usage_error(err.msg()) } path := args.positional_at(0) if path == '' { return usage_error('strip needs a path, e.g. koh strip secrets.env') } // There is no --yes: a person has to be at the keyboard for this one. if !interactive() { return refuse(args.json(), 'strip needs a terminal — an agent can never strip') } store := open_store() or { return refuse(args.json(), err.msg()) } defer { store.close() } mut only_save := '' if given := args.value_or_none('save') { save := history.resolve(store, given) or { return refuse(args.json(), err.msg()) } only_save = save.id } scope := exchange.strip_scope(store, path, only_save) or { return refuse(args.json(), err.msg()) } mut local := exchange.LocalStrip{} if scope.files == 0 { // Nothing left here, but the remote half of an earlier strip may not // have finished; re-running is how it finishes. if !exchange.strip_pending(store, path) { ok('nothing in history matches ${path}') return 0 } note('${path} is already stripped here — finishing the remote') } else { where := if only_save == '' { 'every save' } else { 'save ${only_save}' } println(' ${fmt.red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information') println(' ${fmt.amber('!')} ${fmt.plural(scope.files, 'file')} in ${fmt.plural(scope.saves, 'save')}, ${fmt.plural(scope.objects, 'object')} — ${where}') println(' ${fmt.dim('strip removes the path from history; the file on disk is left alone')}') approved := confirm(false) or { return refuse(args.json(), err.msg()) } if !approved { note('nothing was stripped') return 1 } if !typed_confirm('type the path to confirm:', path) { note('nothing was stripped — the path did not match') return 1 } local = exchange.strip_local(store, path, only_save) or { return refuse(args.json(), err.msg()) } } // A remote failure is reported, never fatal: the local strip has happened, // and running the same strip again finishes the remote. sent := send_strip_to_remote(store, path) // A whole-history strip bans the path; the rule in `.kohignore` says so where a person reads, // and travels with the next save. ignored := only_save == '' && (snapshot.add_ignore(store.root, path) or { return refuse(args.json(), err.msg()) }) if args.json() { emit_json(json2.encode(StripJson{ path: path save: only_save files_removed: local.files_removed objects_dropped: int(local.objects_dropped) records_rewritten: sent.remote.records remote_objects_dropped: sent.remote.objects remote_failed: sent.failed })) return 0 } if scope.files > 0 { ok('stripped ${path} from ${fmt.plural(local.files_removed, 'file')} across ${fmt.plural(scope.saves, 'save')}') note('${fmt.plural(int(local.objects_dropped), 'object')} dropped') } if !sent.skipped { if sent.failed == '' { note('the remote was updated: ${fmt.plural(sent.remote.records, 'save record')} rewritten, ${fmt.plural(sent.remote.objects, 'object')} dropped') } else { warn('the remote could not be updated: ${sent.failed} — run the same strip again to finish it') } } if only_save != '' { note('remember to delete ${path} or add it to ${snapshot.ignore_file}') } else if ignored { note('added /${path} to ${snapshot.ignore_file} — it can never be saved again') } return 0 } struct SendOutcome { mut: remote exchange.SendReport failed string skipped bool } // send_strip_to_remote tells this machine's remote about a strip. fn send_strip_to_remote(store &storage.Store, path string) SendOutcome { if !machine.has_remote() { return SendOutcome{ skipped: true } } mut backend := watched_backend('updating the remote') or { return SendOutcome{ failed: err.msg() } } report := exchange.send_strip(store, mut backend, path) or { return SendOutcome{ failed: err.msg() } } return SendOutcome{ remote: report } } struct StripJson { path string save string files_removed int objects_dropped int records_rewritten int remote_objects_dropped int remote_failed string }