12module cli34import os5import src.build6import src.exchange7import src.fmt8import src.events9import src.history10import src.snapshot11import src.storage1213fn doctor_history(saves []history.Save, head string) []string {14 mut problems := []string{}15 mut known := map[string]history.Save{}16 for save in saves {17 known[save.id] = save18 }19 if head == '' && saves.len > 0 { problems << 'history has saves but no head' }20 if head != '' && head !in known { problems << 'head ${head} names a missing save' }21 mut done := map[string]bool{}22 for save in saves {23 if save.parent != '' && save.parent !in known {24 problems << 'save ${save.id} has missing parent ${save.parent}'25 }26 if save.restores != '' && save.restores !in known {27 problems << 'save ${save.id} restores missing save ${save.restores}'28 }29 mut cursor := save.id30 mut seen := map[string]bool{}31 for cursor != '' && cursor in known && cursor !in done {32 if cursor in seen {33 problems << 'history has a parent cycle at ${cursor}'34 break35 }36 seen[cursor] = true37 cursor = known[cursor].parent38 }39 for id in seen.keys() {40 done[id] = true41 }42 }43 return problems44}454647fn missing_saves(saves []history.Save, head string) []string {48 mut known := map[string]bool{}49 for save in saves {50 known[save.id] = true51 }52 mut missing := []string{}53 for save in saves {54 for id in [save.parent, save.restores] {55 if id != '' && id !in known && id !in missing {56 missing << id57 }58 }59 }60 if head != '' && head !in known && head !in missing {61 missing << head62 }63 return missing64}6566fn doctor_files(id string, files []snapshot.FileEntry, event_list []events.Event,67 mut report DoctorJson) {68 mut paths := map[string]bool{}69 for file in files {70 paths[file.path] = true71 }72 for file in files {73 if !snapshot.legal_path(file.path) {74 report.problems << 'save [${id}] has unsafe path ${snapshot.escape_path(file.path)} — koh strip ${shell_quote(file.path)}'75 }76 mut parent := os.dir(file.path)77 for parent != '.' && parent != '' && parent != '/' {78 if parent in paths {79 report.problems << 'save ${id}: ${snapshot.escape_path(parent)} is both a file and a directory'80 break81 }82 parent = os.dir(parent)83 }84 for event in event_list {85 if events.valid_strip(event) && (event.save_id == '' || event.save_id == id)86 && (file.path == event.path || file.path.starts_with('${event.path.trim_right('/')}/')) {87 report.problems << 'save ${id} still contains stripped path ${snapshot.escape_path(file.path)}'88 report.strip_residue = true89 }90 }91 }92}9394fn doctor_events(event_list []events.Event, saves []history.Save, mut report DoctorJson) {95 ids := saves.map(it.id)96 mut flags := map[string]events.Event{}97 for event in event_list {98 if event.created_at < 0 { report.problems << 'event ${event.id} has a negative timestamp' }99 if event.kind !in [events.kind_flag, events.kind_clear, events.kind_strip] {100 report.problems << 'event ${event.id} has unknown kind ${event.kind}'101 } else if event.kind != events.kind_strip && event.save_id == '' {102 report.problems << 'event ${event.id} has no save target'103 } else if event.kind == events.kind_strip && event.path == '' {104 report.problems << 'strip event ${event.id} has no path'105 }106 107 if event.save_id != '' && event.save_id !in ids {108 report.warnings << 'event ${event.id} targets unavailable save ${event.save_id}; nothing to do'109 }110 if event.kind in [events.kind_flag, events.kind_clear] {111 old := flags[event.save_id] or { events.Event{} }112 if event.created_at > old.created_at || (event.created_at == old.created_at && event.id > old.id) {113 flags[event.save_id] = event114 }115 }116 }117 report.events = event_list.len118 report.flagged_saves = flags.values().filter(it.kind == events.kind_flag).len119 if head_flag := flags[report.head_id] {120 report.head_flagged = head_flag.kind == events.kind_flag121 if report.head_flagged {122 report.warnings << 'head is flagged: ${head_flag.reason}; koh flag --clear removes the flag'123 }124 }125}126127fn doctor_local(store &storage.Store, mut report DoctorJson) ! {128 stats := store.doctor_stats()!129 report.metadata_available = true130 report.objects = int(stats.objects)131 report.saves = int(stats.saves)132 report.checkpoints = int(stats.checkpoints)133 report.cached_files = int(stats.cached_files)134 report.file_entries = stats.file_entries135 report.object_bytes = stats.object_bytes136 report.packed_bytes = stats.packed_bytes137 report.unreferenced_objects = stats.unused138 report.source = stats.state['source']139 report.remote_repo = stats.state['remote_repo']140 report.last_offered_head = stats.state['remote_head']141 report.head_id = stats.state[storage.head_key]142 report.restore_unfinished = 'restoring' in stats.state143 report.pending_strips = stats.pending144 report.check('references and metadata', stats.problems)145 saves := history.all_saves(store)!146 report.check('history links and cycles', doctor_history(saves, report.head_id))147 report.missing_saves = missing_saves(saves, report.head_id)148 mut known := map[string]history.Save{}149 for save in saves {150 known[save.id] = save151 }152 mut seen := map[string]bool{}153 mut cursor := report.head_id154 for cursor != '' && cursor in known && cursor !in seen {155 seen[cursor] = true156 cursor = known[cursor].parent157 }158 report.line_saves = seen.len159 report.off_line_saves = saves.len - seen.len160 if head := known[report.head_id] {161 report.head = head162 report.head_files = history.files_of(store, head.id)!.len163 }164 event_list := events.all(store)!165 before := report.problems.len166 doctor_events(event_list, saves, mut report)167 for save in saves {168 doctor_files(save.id, history.files_of(store, save.id)!, event_list, mut report)169 if save.replayed_from != '' && save.replayed_from !in known {170 report.warnings << 'save ${save.id} was replayed from unavailable save ${save.replayed_from}; nothing to do'171 }172 }173 for path in stats.pending {174 exchange.read_pending(store, path) or { report.problems << err.msg() }175 }176 report.check('saved paths and events', report.problems[before..].clone())177 if report.restore_unfinished {178 report.check('restore state', [snapshot.restore_unfinished])179 } else {180 report.check('restore state', []string{})181 }182 for path in report.pending_strips {183 report.warnings << 'remote strip unfinished for ${snapshot.escape_path(path)}; retry koh strip ${shell_quote(path)}'184 }185 if stats.unused > 0 {186 report.warnings << '${fmt.plural(int(stats.unused), 'unreferenced object')} retained; no save uses them and koh doctor can remove them'187 }188}189190fn doctor_working_tree(store &storage.Store, mut report DoctorJson) ! {191 store.drop_cache()!192 report.cache_cleared = true193 if !report.metadata_available || (report.saves > 0 && report.head.id == '') {194 report.checks << DoctorCheck{'working tree', 'skipped', 'head is missing', ''}195 return196 }197 for path in [os.join_path(store.root, snapshot.ignore_file), snapshot.global_ignore_path()] {198 if !os.exists(path) {199 continue200 }201 text := os.read_file(path) or {202 report.check('ignore rules', [err.msg()])203 continue204 }205 report.ignore_files[path] = text.split_into_lines().filter(it.trim_space() != '' && !it.trim_space().starts_with('#')).len206 }207 files := history.files_of(store, report.head_id)!208 ig := snapshot.load(store.root)209 report.ignored_saved_files = files.filter(ig.matches(it.path, false)).map(it.path)210 report.changes = compare_with_head(store, report.head)!211 mut problems := []string{}212 snapshot.check_links(store.root, files) or { problems << err.msg() }213 for path in report.changes.unsafe_paths {214 problems << 'unsafe working name ${path}; this file cannot be saved'215 }216 report.check('working tree safety', problems)217 report.checks << DoctorCheck{'working tree changes', 'pass', '${report.changes.added.len} added, ${report.changes.modified.len} modified, ${report.changes.removed.len} removed', ''}218 if report.ignored_saved_files.len > 0 {219 report.warnings << 'ignore rules now cover ${fmt.plural(report.ignored_saved_files.len, 'saved file')}; older versions remain in history, and koh strip removes them if they must go'220 }221}222223fn doctor_build(root string, mut report DoctorJson) {224 if !os.exists(os.join_path(root, build.config_name)) {225 report.checks << DoctorCheck{'build configuration', 'skipped', 'no koh.build', ''}226 return227 }228 config := build.read_config(root) or {229 report.check('build configuration', [err.msg()])230 return231 }232 report.build_platforms = config.platforms233 report.project_version = build.project_version(root) or {234 report.check('build configuration', [err.msg()])235 return236 }237 mut paths := map[string]bool{}238 mut problems := []string{}239 for platform in config.platforms {240 path := build.output_path(config, platform, report.project_version)241 if path in paths { problems << 'multiple platforms share build output ${path}' }242 paths[path] = true243 }244 report.check('build configuration', problems)245}