doctor_checks.v

9.3 KB · raw

1//! Doctor's local history, event and working-tree checks.2module 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}4546// Saves that history names (as parent, restored save or head) but does not hold.47fn 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				if event.path !in report.strip_residue_paths {90					report.strip_residue_paths << event.path91				}92			}93		}94	}95}9697fn doctor_events(event_list []events.Event, saves []history.Save, mut report DoctorJson) {98	ids := saves.map(it.id)99	mut flags := map[string]events.Event{}100	for event in event_list {101		if !exchange.valid_event_id(event.id) {102			report.problems << 'event ${event.id} has an invalid ID'103		}104		if event.created_at < 0 { report.problems << 'event ${event.id} has a negative timestamp' }105		if event.kind !in [events.kind_flag, events.kind_clear, events.kind_strip] {106			report.problems << 'event ${event.id} has unknown kind ${event.kind}'107		} else if event.kind != events.kind_strip && event.save_id == '' {108			report.problems << 'event ${event.id} has no save target'109		} else if event.kind == events.kind_strip && event.path == '' {110			report.problems << 'strip event ${event.id} has no path'111		}112		// Sync may retain an event for an off-line save this receiver never fetched.113		if event.save_id != '' && event.save_id !in ids {114			report.warnings << 'event ${event.id} targets unavailable save ${event.save_id}; nothing to do'115		}116		if event.kind in [events.kind_flag, events.kind_clear] {117			old := flags[event.save_id] or { events.Event{} }118			if event.created_at > old.created_at || (event.created_at == old.created_at && event.id > old.id) {119				flags[event.save_id] = event120			}121		}122	}123	report.events = event_list.len124	report.flagged_saves = flags.values().filter(it.kind == events.kind_flag).len125	if head_flag := flags[report.head_id] {126		report.head_flagged = head_flag.kind == events.kind_flag127		if report.head_flagged {128			report.warnings << 'head is flagged: ${head_flag.reason}; koh flag --clear removes the flag'129		}130	}131}132133fn doctor_local(store &storage.Store, mut report DoctorJson) ! {134	stats := store.doctor_stats()!135	report.metadata_available = true136	report.objects = int(stats.objects)137	report.saves = int(stats.saves)138	report.checkpoints = int(stats.checkpoints)139	report.cached_files = int(stats.cached_files)140	report.file_entries = stats.file_entries141	report.object_bytes = stats.object_bytes142	report.packed_bytes = stats.packed_bytes143	report.unreferenced_objects = stats.unused144	report.source = stats.state['source']145	report.remote_repo = stats.state['remote_repo']146	report.last_offered_head = stats.state['remote_head']147	report.head_id = stats.state[storage.head_key]148	report.restore_unfinished = 'restoring' in stats.state149	report.pending_strips = stats.pending150	report.upload_unsettled = exchange.unsettled_key in stats.state151	report.check('references and metadata', stats.problems)152	saves := history.all_saves(store)!153	report.check('history links and cycles', doctor_history(saves, report.head_id))154	report.missing_saves = missing_saves(saves, report.head_id)155	mut known := map[string]history.Save{}156	for save in saves {157		known[save.id] = save158	}159	mut seen := map[string]bool{}160	mut cursor := report.head_id161	for cursor != '' && cursor in known && cursor !in seen {162		seen[cursor] = true163		cursor = known[cursor].parent164	}165	report.line_saves = seen.len166	report.off_line_saves = saves.len - seen.len167	if head := known[report.head_id] {168		report.head = head169		report.head_files = history.files_of(store, head.id)!.len170	}171	event_list := events.all(store)!172	before := report.problems.len173	doctor_events(event_list, saves, mut report)174	for save in saves {175		doctor_files(save.id, history.files_of(store, save.id)!, event_list, mut report)176		if save.replayed_from != '' && save.replayed_from !in known {177			report.warnings << 'save ${save.id} was replayed from unavailable save ${save.replayed_from}; nothing to do'178		}179	}180	for path in stats.pending {181		exchange.read_pending(store, path) or { report.problems << err.msg() }182	}183	exchange.unsettled(store) or { report.problems << err.msg() }184	report.check('saved paths and events', report.problems[before..].clone())185	if report.restore_unfinished {186		report.check('restore state', [snapshot.restore_unfinished])187	} else {188		report.check('restore state', []string{})189	}190	for path in report.pending_strips {191		report.warnings << 'remote strip unfinished for ${snapshot.escape_path(path)}; retry koh strip ${shell_quote(path)}'192	}193	if report.upload_unsettled {194		report.warnings << 'an upload stopped before it could remove what a remote strip covers; after koh steal, koh doctor can finish it'195	}196	if stats.unused > 0 {197		report.warnings << '${fmt.plural(int(stats.unused), 'unreferenced object')} retained; no save uses them and koh doctor can remove them'198	}199}200201fn doctor_working_tree(store &storage.Store, mut report DoctorJson) ! {202	store.drop_cache()!203	report.cache_cleared = true204	if !report.metadata_available || (report.saves > 0 && report.head.id == '') {205		report.checks << DoctorCheck{'working tree', 'skipped', 'head is missing', ''}206		return207	}208	for path in [os.join_path(store.root, snapshot.ignore_file), snapshot.global_ignore_path()] {209		if !os.exists(path) {210			continue211		}212		text := os.read_file(path) or {213			report.check('ignore rules', [err.msg()])214			continue215		}216		report.ignore_files[path] = text.split_into_lines().filter(it.trim_space() != '' && !it.trim_space().starts_with('#')).len217	}218	files := history.files_of(store, report.head_id)!219	ig := snapshot.load(store.root)220	report.ignored_saved_files = files.filter(ig.matches(it.path, false)).map(it.path)221	report.changes = compare_with_head(store, report.head)!222	mut problems := []string{}223	snapshot.check_links(store.root, files) or { problems << err.msg() }224	for path in report.changes.unsafe_paths {225		problems << 'unsafe working name ${path}; this file cannot be saved'226	}227	report.check('working tree safety', problems)228	report.checks << DoctorCheck{'working tree changes', 'pass', '${report.changes.added.len} added, ${report.changes.modified.len} modified, ${report.changes.removed.len} removed', ''}229	if report.ignored_saved_files.len > 0 {230		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'231	}232}233234fn doctor_build(root string, mut report DoctorJson) {235	if !os.exists(os.join_path(root, build.config_name)) {236		report.checks << DoctorCheck{'build configuration', 'skipped', 'no koh.build', ''}237		return238	}239	config := build.read_config(root) or {240		report.check('build configuration', [err.msg()])241		return242	}243	report.build_platforms = config.platforms244	report.project_version = build.project_version(root) or {245		report.check('build configuration', [err.msg()])246		return247	}248	mut paths := map[string]bool{}249	mut problems := []string{}250	for platform in config.platforms {251		path := build.output_path(config, platform, report.project_version)252		if path in paths { problems << 'multiple platforms share build output ${path}' }253		paths[path] = true254	}255	report.check('build configuration', problems)256}