12345678module 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 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 84 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}105106107fn 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}