1234567891011module cli1213import json214import src.fmt15import src.history16import src.machine17import src.snapshot18import src.storage19import src.exchange2021struct StripPlan {22 path string23 scope exchange.Scope24 pending bool25}262728pub fn cmd_strip(raw []string) int {29 args := parse('strip', raw, Options{30 values: ['save']31 flags: ['json', 'yes']32 }) or { return usage_error(err.msg()) }33 if args.positional.len == 0 {34 return usage_error('strip needs paths, e.g. koh strip secrets.env private/')35 }36 37 if !interactive() {38 return refuse(args.json(), 'strip needs a terminal — an agent can never strip')39 }40 store := open_store() or { return refuse(args.json(), err.msg()) }41 defer { store.close() }42 mut only_save := ''43 if given := args.value_or_none('save') {44 save := history.resolve(store, given) or { return refuse(args.json(), err.msg()) }45 only_save = save.id46 }47 mut plans := []StripPlan{}48 for item in args.positional {49 50 path := if exchange.strip_pending(store, item) { item } else { item.trim_right('/') }51 if plans.any(it.path == path) {52 continue53 }54 scope := exchange.strip_scope(store, path, only_save) or {55 return refuse(args.json(), err.msg())56 }57 pending := exchange.strip_pending(store, path)58 malformed := path == '' || path.starts_with('/')59 || path.split('/').any(it in ['', '.', '..'])60 61 if path == '' || (malformed && scope.files == 0 && !pending) {62 return refuse(args.json(), 'strip needs repo-relative paths: ${snapshot.escape_path(item)}')63 }64 plans << StripPlan{ path: path, scope: scope, pending: pending }65 }66 where := if only_save == '' { 'every save' } else { 'save ${only_save}' }67 for plan in plans {68 if plan.scope.files == 0 {69 if plan.pending {70 strip_note(args.json(), '${snapshot.escape_path(plan.path)} is already stripped here — finishing the remote')71 } else {72 strip_note(args.json(), 'nothing in history matches ${snapshot.escape_path(plan.path)}')73 }74 } else {75 strip_note(args.json(), 'strip ${snapshot.escape_path(plan.path)}: ${fmt.plural(plan.scope.files, 'file')} in ${fmt.plural(plan.scope.saves, 'save')}, ${fmt.plural(plan.scope.objects, 'object')} — ${where}')76 }77 }78 if plans.any(it.scope.files > 0) {79 strip_note(args.json(), '${fmt.red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information')80 strip_note(args.json(), 'strip removes paths from history; the files on disk are left alone')81 approved := confirm(args.flag('yes')) or { return refuse(args.json(), err.msg()) }82 if !approved {83 return refuse(args.json(), 'nothing was stripped')84 }85 for plan in plans {86 if plan.scope.files == 0 {87 continue88 }89 strip_note(args.json(), 'confirm ${snapshot.escape_path(plan.path)}')90 if !typed_confirm('type the path to confirm:', plan.path) {91 return refuse(args.json(), 'nothing was stripped — the path did not match')92 }93 }94 }95 active := plans.filter(it.scope.files > 0 || it.pending)96 locals := exchange.strip_local_batch(store, active.map(it.path), only_save) or {97 return refuse(args.json(), err.msg())98 }99 mut reports := []StripJson{}100 mut index := 0101 mut exit_code := 0102 for plan in plans {103 if plan.scope.files == 0 && !plan.pending {104 reports << StripJson{ path: plan.path, save: only_save }105 continue106 }107 local := locals[index]108 index++109 path := plan.path110 111 sent := send_strip_to_remote(store, path)112 mut ignore_failed := ''113 ignored := only_save == '' && (local.files_removed > 0 || plan.pending)114 && (snapshot.add_ignore(store.root, path) or {115 ignore_failed = '${err.msg()} — add ${snapshot.ignore_rule(path, false, false)} to ${snapshot.ignore_file} by hand; the history ban still applies'116 exit_code = 1117 false118 })119 reports << StripJson{120 path: path121 save: only_save122 files_removed: local.files_removed123 objects_dropped: int(local.objects_dropped)124 records_rewritten: sent.remote.records125 remote_objects_dropped: sent.remote.objects126 remote_failed: sent.failed127 ignore_failed: ignore_failed128 }129 if args.json() {130 continue131 }132 if plan.scope.files > 0 {133 ok('stripped ${snapshot.escape_path(path)} from ${fmt.plural(local.files_removed, 'file')} across ${fmt.plural(plan.scope.saves, 'save')}')134 strip_note(args.json(), '${fmt.plural(int(local.objects_dropped), 'object')} dropped')135 }136 if !sent.skipped {137 if sent.failed == '' {138 strip_note(args.json(), 'the remote was updated: ${fmt.plural(sent.remote.records, 'save record')} rewritten, ${fmt.plural(sent.remote.objects, 'object')} dropped')139 } else {140 warn('the remote could not be updated: ${sent.failed} — run the same strip again to finish it')141 }142 }143 if ignore_failed != '' {144 warn('could not update ${snapshot.ignore_file} for ${snapshot.escape_path(path)}: ${ignore_failed}')145 }146 if only_save != '' {147 strip_note(args.json(), 'remember to delete ${snapshot.escape_path(path)} or add it to ${snapshot.ignore_file}')148 } else if ignored {149 strip_note(args.json(), 'added /${snapshot.escape_path(path)} to ${snapshot.ignore_file} — it can never be saved again')150 }151 }152 if args.json() {153 if plans.len == 1 {154 emit_json(json2.encode(reports[0]))155 } else {156 emit_json(json2.encode({157 'strips': reports158 }))159 }160 }161 return exit_code162}163164struct SendOutcome {165mut:166 remote exchange.SendReport167 failed string168 skipped bool169}170171172fn send_strip_to_remote(store &storage.Store, path string) SendOutcome {173 if !machine.has_remote() {174 return SendOutcome{175 skipped: true176 }177 }178 mut backend := watched_backend('updating the remote') or {179 return SendOutcome{180 failed: err.msg()181 }182 }183 report := exchange.send_strip(store, mut backend, path) or {184 return SendOutcome{185 failed: err.msg()186 }187 }188 return SendOutcome{189 remote: report190 }191}192193struct StripJson {194 path string195 save string196 files_removed int197 objects_dropped int198 records_rewritten int199 remote_objects_dropped int200 remote_failed string201 ignore_failed string202}203204fn strip_note(as_json bool, message string) {205 if as_json { eprintln(' ${message}') } else { note(message) }206}