cmd_pluck.v

4.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.apply12import src.fmt13import src.history14import src.snapshot15import src.storage1617pub fn cmd_pluck(raw []string) int {18	args := parse('pluck', raw, Options{19		flags: ['reverse', 'dry-run', 'yes', 'json']20	}) or { return usage_error(err.msg()) }21	store := open_store() or { return refuse(args.json(), err.msg()) }22	defer {23		store.close()24	}25	id := args.positional_at(0)26	if id == '' {27		return usage_error('pluck needs a save ID')28	}29	source := history.resolve(store, id) or { return refuse(args.json(), err.msg()) }30	mut filters := []string{}31	if args.positional.len > 1 {32		filters = args.positional[1..]33	}34	// A dry run only reads, so it may look at a dirty tree; a real pluck35	// rewrites files and must not overwrite unsaved work.36	if !args.flag('dry-run') {37		require_clean(store) or { return refuse(args.json(), err.msg()) }38	}39	reverse := args.flag('reverse')40	verb := if reverse { 'pluck reverse' } else { 'pluck' }4142	plan, _ := plan_pluck(store, source, reverse, filters) or {43		return refuse(args.json(), err.msg())44	}4546	if args.flag('dry-run') || plan.applied.len == 0 {47		if args.json() {48			emit_json(json.encode(PluckJson{49				source:     source.id50				reverse:    reverse51				applied:    plan.applied52				applied_ok: true53			}))54			return 055		}56		if plan.applied.len == 0 {57			ok('nothing to apply from [${source.id}]')58			return 059		}60		ok('${verb} [${source.id}] would change:')61		for path in plan.applied {62			println('  ${fmt.amber('~')} ${path}')63		}64		note('${fmt.plural(plan.applied.len, 'file')} — run again without --dry-run to apply')65		return 066	}6768	ok('${verb}: [${source.id}]  ${source.message}')69	for path in plan.applied {70		println('  ${fmt.amber('~')} ${path}')71	}72	note('${fmt.plural(plan.applied.len, 'file')} change')73	approved := confirm(args.flag('yes')) or { return refuse(args.json(), err.msg()) }74	if !approved {75		return 176	}77	// The tree may have changed while the prompt waited, so the pluck acts on a78	// fresh plan; the one above only drew the preview.79	require_clean(store) or { return refuse(args.json(), err.msg()) }80	fresh, current := plan_pluck(store, source, reverse, filters) or {81		return refuse(args.json(), err.msg())82	}83	if fresh.applied.len == 0 {84		return refuse(args.json(),85			'nothing to apply from [${source.id}] any more — the tree changed while waiting')86	}87	// The resulting tree is the working tree with the change applied, so its88	// object bytes come from disk.89	snapshot.mark_restore(store) or { return refuse(args.json(), err.msg()) }90	save := history.commit_files(store, store.root, fresh.files, history.SaveInput{91		message: '${verb}: ${source.id}'92	}) or {93		snapshot.clear_restore_mark(store) or {}94		return refuse(args.json(), err.msg())95	}96	remove := not_in(current, fresh.files)97	report := snapshot.restore_files(store, store.root, snapshot.load(store.root), fresh.files,98		remove) or { return refuse(args.json(), err.msg()) }99	if args.json() {100		emit_json(json.encode(PluckedJson{101			id:      save.id102			source:  source.id103			reverse: reverse104			applied: fresh.applied105			written: report.written106			removed: report.removed107		}))108		return 0109	}110	ok('plucked [${source.id}] into [${save.id}]')111	return 0112}113114// plan_pluck applies the source save's change (or, reversed, its inverse) to115// the working tree, and returns the plan with the tree it was made from.116fn plan_pluck(store &storage.Store, source history.Save, reverse bool, filters []string) !(apply.Plan, []snapshot.FileEntry) {117	source_tree := tree_of(store, source.id)!118	mut parent_tree := snapshot.Tree{}119	if source.parent != '' {120		parent_tree = tree_of(store, source.parent)!121	}122	current := working_tree(store)!123	base := if reverse { source_tree } else { parent_tree }124	target := if reverse { parent_tree } else { source_tree }125	plan := apply.three_way(current.files, base.files, target.files, filters, [])126	if apply.conflicts(plan) {127		return error('${plan.conflict} has changed since [${source.id}]; koh does not merge lines — resolve it by hand, or load the save and re-edit')128	}129	snapshot.check_links(store.root, plan.files)!130	return plan, current.files131}132133// not_in lists the paths of `before` that `after` no longer has.134fn not_in(before []snapshot.FileEntry, after []snapshot.FileEntry) []string {135	mut kept := map[string]bool{}136	for entry in after {137		kept[entry.path] = true138	}139	mut missing := []string{}140	for entry in before {141		if entry.path !in kept {142			missing << entry.path143		}144	}145	return missing146}147148struct PluckJson {149	source     string150	reverse    bool151	applied    []string152	applied_ok bool153}154155struct PluckedJson {156	id      string157	source  string158	reverse bool159	applied []string160	written int161	removed int162}