12module cli34import os5import src.build6import src.events7import src.history8import src.snapshot9import src.storage1011fn doctor_history(saves []history.Save, head string) []string {12 mut problems := []string{}13 mut known := map[string]history.Save{}14 for save in saves {15 known[save.id] = save16 }17 if head == '' && saves.len > 0 { problems << 'history has saves but no head' }18 if head != '' && head !in known { problems << 'head ${head} names a missing save' }19 mut done := map[string]bool{}20 for save in saves {21 if save.parent != '' && save.parent !in known {22 problems << 'save ${save.id} has missing parent ${save.parent}'23 }24 if save.restores != '' && save.restores !in known {25 problems << 'save ${save.id} restores missing save ${save.restores}'26 }27 mut cursor := save.id28 mut seen := map[string]bool{}29 for cursor != '' && cursor in known && cursor !in done {30 if cursor in seen {31 problems << 'history has a parent cycle at ${cursor}'32 break33 }34 seen[cursor] = true35 cursor = known[cursor].parent36 }37 for id in seen.keys() {38 done[id] = true39 }40 }41 return problems42}4344fn doctor_files(id string, files []snapshot.FileEntry, event_list []events.Event,45 mut report DoctorJson) {46 mut paths := map[string]bool{}47 for file in files {48 paths[file.path] = true49 }50 for file in files {51 if !snapshot.legal_path(file.path) {52 report.problems << 'save [${id}] has unsafe path ${snapshot.escape_path(file.path)} — koh strip ${shell_quote(file.path)}'53 }54 mut parent := os.dir(file.path)55 for parent != '.' && parent != '' && parent != '/' {56 if parent in paths {57 report.problems << 'save ${id}: ${snapshot.escape_path(parent)} is both a file and a directory'58 break59 }60 parent = os.dir(parent)61 }62 for event in event_list {63 if event.kind == events.kind_strip && event.path != ''64 && (event.save_id == '' || event.save_id == id)65 && (file.path == event.path || file.path.starts_with('${event.path.trim_right('/')}/')) {66 report.problems << 'save ${id} still contains stripped path ${snapshot.escape_path(file.path)}'67 }68 }69 }70}7172fn doctor_events(event_list []events.Event, saves []history.Save, mut report DoctorJson) {73 ids := saves.map(it.id)74 mut flags := map[string]events.Event{}75 for event in event_list {76 if event.created_at < 0 { report.problems << 'event ${event.id} has a negative timestamp' }77 if event.kind !in [events.kind_flag, events.kind_clear, events.kind_strip] {78 report.problems << 'event ${event.id} has unknown kind ${event.kind}'79 } else if event.kind != events.kind_strip && event.save_id == '' {80 report.problems << 'event ${event.id} has no save target'81 } else if event.kind == events.kind_strip && event.path == '' {82 report.problems << 'strip event ${event.id} has no path'83 }84 85 if event.save_id != '' && event.save_id !in ids {86 report.warnings << 'event ${event.id} targets unavailable save ${event.save_id}'87 }88 if event.kind in [events.kind_flag, events.kind_clear] {89 old := flags[event.save_id] or { events.Event{} }90 if event.created_at > old.created_at || (event.created_at == old.created_at && event.id > old.id) {91 flags[event.save_id] = event92 }93 }94 }95 report.events = event_list.len96 report.flagged_saves = flags.values().filter(it.kind == events.kind_flag).len97 if head_flag := flags[report.head_id] {98 report.head_flagged = head_flag.kind == events.kind_flag99 if report.head_flagged { report.warnings << 'head is flagged: ${head_flag.reason}' }100 }101}102103fn doctor_local(store &storage.Store, mut report DoctorJson) ! {104 stats := store.doctor_stats()!105 report.metadata_available = true106 report.objects = int(stats.objects)107 report.saves = int(stats.saves)108 report.checkpoints = int(stats.checkpoints)109 report.cached_files = int(stats.cached_files)110 report.file_entries = stats.file_entries111 report.object_bytes = stats.object_bytes112 report.packed_bytes = stats.packed_bytes113 report.unreferenced_objects = stats.unused114 report.source = stats.state['source']115 report.remote_repo = stats.state['remote_repo']116 report.last_offered_head = stats.state['remote_head']117 report.head_id = stats.state[storage.head_key]118 report.restore_unfinished = 'restoring' in stats.state119 report.pending_strips = stats.pending120 report.check('references and metadata', stats.problems)121 saves := history.all_saves(store)!122 report.check('history links and cycles', doctor_history(saves, report.head_id))123 mut known := map[string]history.Save{}124 for save in saves {125 known[save.id] = save126 }127 mut seen := map[string]bool{}128 mut cursor := report.head_id129 for cursor != '' && cursor in known && cursor !in seen {130 seen[cursor] = true131 cursor = known[cursor].parent132 }133 report.line_saves = seen.len134 report.off_line_saves = saves.len - seen.len135 if head := known[report.head_id] {136 report.head = head137 report.head_files = history.files_of(store, head.id)!.len138 }139 event_list := events.all(store)!140 before := report.problems.len141 doctor_events(event_list, saves, mut report)142 for save in saves {143 doctor_files(save.id, history.files_of(store, save.id)!, event_list, mut report)144 if save.replayed_from != '' && save.replayed_from !in known {145 report.warnings << 'save ${save.id} was replayed from unavailable save ${save.replayed_from}'146 }147 }148 report.check('saved paths and events', report.problems[before..].clone())149 if report.restore_unfinished {150 report.check('restore state', [snapshot.restore_unfinished])151 } else {152 report.check('restore state', []string{})153 }154 for path in report.pending_strips {155 report.warnings << 'remote strip unfinished for ${snapshot.escape_path(path)}; retry koh strip ${shell_quote(path)}'156 }157 if stats.unused > 0 {158 report.warnings << '${stats.unused} unreferenced objects retained; Doctor does not remove history'159 }160}161162fn doctor_working_tree(store &storage.Store, mut report DoctorJson) ! {163 store.drop_cache()!164 report.cache_cleared = true165 if !report.metadata_available || (report.saves > 0 && report.head.id == '') {166 report.checks << DoctorCheck{'working tree', 'skipped', 'head is missing'}167 return168 }169 for path in [os.join_path(store.root, snapshot.ignore_file), snapshot.global_ignore_path()] {170 if !os.exists(path) {171 continue172 }173 text := os.read_file(path) or {174 report.check('ignore rules', [err.msg()])175 continue176 }177 report.ignore_files[path] = text.split_into_lines().filter(it.trim_space() != '' && !it.trim_space().starts_with('#')).len178 }179 files := history.files_of(store, report.head_id)!180 ig := snapshot.load(store.root)181 report.ignored_saved_files = files.filter(ig.matches(it.path, false)).map(it.path)182 report.changes = compare_with_head(store, report.head)!183 mut problems := []string{}184 snapshot.check_links(store.root, files) or { problems << err.msg() }185 for path in report.changes.unsafe_paths {186 problems << 'unsafe working name ${path}; this file cannot be saved'187 }188 report.check('working tree safety', problems)189 report.checks << DoctorCheck{'working tree changes', 'pass', '${report.changes.added.len} added, ${report.changes.modified.len} modified, ${report.changes.removed.len} removed'}190 if report.ignored_saved_files.len > 0 {191 report.warnings << '${report.ignored_saved_files.len} saved files now match ignore rules; older versions remain in history'192 }193}194195fn doctor_build(root string, mut report DoctorJson) {196 if !os.exists(os.join_path(root, build.config_name)) {197 report.checks << DoctorCheck{'build configuration', 'skipped', 'no koh.build'}198 return199 }200 config := build.read_config(root) or {201 report.check('build configuration', [err.msg()])202 return203 }204 report.build_platforms = config.platforms205 report.project_version = build.project_version(root) or {206 report.check('build configuration', [err.msg()])207 return208 }209 mut paths := map[string]bool{}210 mut problems := []string{}211 for platform in config.platforms {212 path := build.output_path(config, platform, report.project_version)213 if path in paths { problems << 'multiple platforms share build output ${path}' }214 paths[path] = true215 }216 report.check('build configuration', problems)217}