cmd_pluck.v

5.1 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 json211import src.apply12import src.fmt13import src.history14import src.snapshot15import src.storage1617// cmd_pluck puts one save's change on the current tree (--reverse takes it off, --dry-run only reports).18// A real pluck refuses unsaved changes, and any conflict refuses the whole pluck.19pub fn cmd_pluck(raw []string) int {20	args := parse('pluck', raw, Options{21		flags: ['reverse', 'dry-run', 'yes', 'json']22	}) or { return usage_error(err.msg()) }23	store := open_store() or { return refuse(args.json(), err.msg()) }24	defer {25		store.close()26	}27	id := args.positional_at(0)28	if id == '' {29		return usage_error('pluck needs a save ID')30	}31	source := history.resolve(store, id) or { return refuse(args.json(), err.msg()) }32	mut filters := []string{}33	if args.positional.len > 1 {34		filters = args.positional[1..]35	}36	// A dry run only reads, so it may look at a dirty tree; a real pluck37	// rewrites files and must not overwrite unsaved work.38	if !args.flag('dry-run') {39		require_clean(store) or { return refuse(args.json(), err.msg()) }40	}41	reverse := args.flag('reverse')42	verb := if reverse { 'pluck reverse' } else { 'pluck' }4344	plan, _ := plan_pluck(store, source, reverse, filters) or {45		return refuse(args.json(), err.msg())46	}4748	if args.flag('dry-run') || plan.applied.len == 0 {49		if args.json() {50			emit_json(json2.encode(PluckJson{51				source:     source.id52				reverse:    reverse53				applied:    plan.applied54				applied_ok: true55			}))56			return 057		}58		if plan.applied.len == 0 {59			ok('nothing to apply from [${source.id}]')60			return 061		}62		ok('${verb} [${source.id}] would change:')63		for path in plan.applied {64			println('  ${fmt.amber('~')} ${path}')65		}66		note('${fmt.plural(plan.applied.len, 'file')} — run again without --dry-run to apply')67		return 068	}6970	ok('${verb}: [${source.id}]  ${source.message}')71	for path in plan.applied {72		println('  ${fmt.amber('~')} ${path}')73	}74	note('${fmt.plural(plan.applied.len, 'file')} change')75	approved := confirm(args.flag('yes')) or { return refuse(args.json(), err.msg()) }76	if !approved {77		return 178	}79	// The tree may have changed while the prompt waited, so the pluck acts on a80	// fresh plan; the one above only drew the preview.81	require_clean(store) or { return refuse(args.json(), err.msg()) }82	fresh, current := plan_pluck(store, source, reverse, filters) or {83		return refuse(args.json(), err.msg())84	}85	if fresh.applied.len == 0 {86		return refuse(args.json(),87			'nothing to apply from [${source.id}] any more — the tree changed while waiting')88	}89	// The resulting tree is the working tree with the change applied, so its90	// object bytes come from disk.91	snapshot.mark_restore(store) or { return refuse(args.json(), err.msg()) }92	save := history.save_tree(store, store.root, fresh.files, history.SaveInput{93		message: '${verb}: ${source.id}'94	}) or {95		snapshot.clear_restore_mark(store) or {}96		return refuse(args.json(), err.msg())97	}98	remove := not_in(current, fresh.files)99	report := snapshot.restore_files(store, store.root, snapshot.load(store.root), fresh.files,100		remove) or { return refuse(args.json(), err.msg()) }101	if args.json() {102		emit_json(json2.encode(PluckedJson{103			id:      save.id104			source:  source.id105			reverse: reverse106			applied: fresh.applied107			written: report.written108			removed: report.removed109		}))110		return 0111	}112	ok('plucked [${source.id}] into [${save.id}]')113	return 0114}115116// plan_pluck applies the source save's change (or, reversed, its inverse) to117// the working tree, and returns the plan with the tree it was made from.118fn plan_pluck(store &storage.Store, source history.Save, reverse bool, filters []string) !(apply.Plan, []snapshot.FileEntry) {119	source_tree := tree_of(store, source.id)!120	mut parent_tree := snapshot.Tree{}121	if source.parent != '' {122		parent_tree = tree_of(store, source.parent)!123	}124	current := working_tree(store)!125	base := if reverse { source_tree } else { parent_tree }126	target := if reverse { parent_tree } else { source_tree }127	plan := apply.three_way(current.files, base.files, target.files, filters, [])128	if apply.conflicts(plan) {129		return error('${plan.conflict} has changed since [${source.id}]; koh does not blend lines — resolve it by hand, or load the save and re-edit')130	}131	snapshot.check_links(store.root, plan.files)!132	return plan, current.files133}134135// not_in lists the paths of `before` that `after` no longer has.136fn not_in(before []snapshot.FileEntry, after []snapshot.FileEntry) []string {137	mut kept := map[string]bool{}138	for entry in after {139		kept[entry.path] = true140	}141	mut missing := []string{}142	for entry in before {143		if entry.path !in kept {144			missing << entry.path145		}146	}147	return missing148}149150struct PluckJson {151	source     string152	reverse    bool153	applied    []string154	applied_ok bool155}156157struct PluckedJson {158	id      string159	source  string160	reverse bool161	applied []string162	written int163	removed int164}