cmd_pluck.v

3.9 KB · raw

1//! `koh pluck` — put one save's change onto the current tree.2//!3//! The engine is per file, never per line: for every path in scope the current4//! content must match either the save's parent (clean apply) or the save's own5//! version (already applied). Anything else is a hard conflict, reported by6//! name, and nothing is applied.78module cli910import json11import src.fmt12import src.apply13import src.history14import src.snapshot1516pub fn cmd_pluck(raw []string) int {17	args := parse('pluck', raw, Options{18		flags: ['reverse', 'dry-run', 'yes', 'json']19	}) or { return usage_error(err.msg()) }20	store := open_store() or { return refuse(args.json(), err.msg()) }21	defer {22		store.close()23	}24	id := args.positional_at(0)25	if id == '' {26		return usage_error('pluck needs a save ID')27	}28	source := history.resolve(store, id) or { return refuse(args.json(), err.msg()) }29	mut filters := []string{}30	if args.positional.len > 1 {31		filters = args.positional[1..]32	}33	reverse := args.flag('reverse')34	verb := if reverse { 'pluck reverse' } else { 'pluck' }3536	source_tree := tree_of(store, source.id) or { return refuse(args.json(), err.msg()) }37	mut parent_tree := snapshot.Tree{}38	if source.parent != '' {39		parent_tree = tree_of(store, source.parent) or { return refuse(args.json(), err.msg()) }40	}41	current := working_tree(store) or { return refuse(args.json(), err.msg()) }42	// Forward pluck applies the save's change; --reverse applies its inverse.43	base := if reverse { source_tree } else { parent_tree }44	target := if reverse { parent_tree } else { source_tree }4546	plan := apply.three_way(current.files, base.files, target.files, filters, [])47	if apply.conflicts(plan) {48		return refuse(args.json(),49			'${plan.conflict} has changed since [${source.id}]; koh does not merge lines — resolve it by hand, or load the save and re-edit')50	}5152	if args.flag('dry-run') || plan.applied.len == 0 {53		if args.json() {54			emit_json(json.encode(PluckJson{55				source:     source.id56				reverse:    reverse57				applied:    plan.applied58				applied_ok: true59			}))60			return 061		}62		if plan.applied.len == 0 {63			ok('nothing to apply from [${source.id}]')64			return 065		}66		ok('${verb} [${source.id}] would change:')67		for path in plan.applied {68			println('  ${amber('~')} ${path}')69		}70		note('${fmt.plural(plan.applied.len, 'file')} — run again without --dry-run to apply')71		return 072	}7374	ok('${verb}: [${source.id}]  ${source.message}')75	for path in plan.applied {76		println('  ${amber('~')} ${path}')77	}78	note('${fmt.plural(plan.applied.len, 'file')} change')79	approved := confirm(args.flag('yes')) or { return refuse(args.json(), err.msg()) }80	if !approved {81		return 182	}83	// The resulting tree is the working tree with the change applied, so its84	// object bytes come from disk.85	save := history.commit_files(store, store.root, plan.files, history.SaveInput{86		message: '${verb}: ${source.id}'87	}) or { return refuse(args.json(), err.msg()) }88	remove := not_in(current.files, plan.files)89	report := snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files,90		remove) or { return refuse(args.json(), err.msg()) }91	if args.json() {92		emit_json(json.encode(PluckedJson{93			id:      save.id94			source:  source.id95			reverse: reverse96			applied: plan.applied97			written: report.written98			removed: report.removed99		}))100		return 0101	}102	ok('plucked [${source.id}] into [${save.id}]')103	return 0104}105106// not_in lists the paths of `before` that `after` no longer has.107fn not_in(before []snapshot.FileEntry, after []snapshot.FileEntry) []string {108	mut kept := map[string]bool{}109	for entry in after {110		kept[entry.path] = true111	}112	mut missing := []string{}113	for entry in before {114		if entry.path !in kept {115			missing << entry.path116		}117	}118	return missing119}120121struct PluckJson {122	source     string123	reverse    bool124	applied    []string125	applied_ok bool126}127128struct PluckedJson {129	id      string130	source  string131	reverse bool132	applied []string133	written int134	removed int135}