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 json15import src.fmt16import src.history17import src.machine18import src.sync19import src.snapshot20import src.storage2122pub 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 := sync.strip_scope(store, path, only_save) or { return refuse(args.json(), err.msg()) }45	mut local := sync.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 !sync.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('  ${red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information')57		println('  ${amber('!')} ${fmt.plural(scope.files, 'file')} in ${fmt.plural(scope.saves,58			'save')}, ${fmt.plural(scope.objects, 'object')} — ${where}')59		println('  ${dim('strip removes the path from history; the file on disk is left alone')}')60		approved := confirm(false) or { return refuse(args.json(), err.msg()) }61		if !approved {62			note('nothing was stripped')63			return 164		}65		if !typed_confirm('type the path to confirm:', path) {66			note('nothing was stripped — the path did not match')67			return 168		}69		local = sync.strip_local(store, path, only_save) or {70			return refuse(args.json(), err.msg())71		}72	}73	// A remote failure is reported, never fatal: the local strip has happened,74	// and running the same strip again finishes the remote.75	push := push_strip_to_remote(store, path)76	if args.json() {77		emit_json(json.encode(StripJson{78			path:                   path79			save:                   only_save80			files_removed:          local.files_removed81			objects_dropped:        int(local.objects_dropped)82			records_rewritten:      push.pushed.records83			remote_objects_dropped: push.pushed.objects84			remote_failed:          push.failed85		}))86		return 087	}88	if scope.files > 0 {89		ok('stripped ${path} from ${fmt.plural(local.files_removed, 'file')} across ${fmt.plural(scope.saves,90			'save')}')91		note('${local.objects_dropped} objects dropped')92	}93	if !push.skipped {94		if push.failed == '' {95			note('the remote was updated: ${push.pushed.records} save records rewritten, ${push.pushed.objects} objects dropped')96		} else {97			warn('the remote could not be updated: ${push.failed} — run the same strip again to finish it')98		}99	}100	note('remember to delete ${path} or add it to ${snapshot.ignore_file}')101	return 0102}103104struct PushOutcome {105mut:106	pushed  sync.PushReport107	failed  string108	skipped bool109}110111// push_strip_to_remote tells this machine's remote about a strip.112fn push_strip_to_remote(store &storage.Store, path string) PushOutcome {113	if !machine.has_remote() {114		return PushOutcome{115			skipped: true116		}117	}118	mut backend := this_machine_backend() or { return PushOutcome{119		failed: err.msg()120	} }121	report := sync.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}