doctor_checks.v

8.0 KB · raw

1//! Doctor's local history, event and working-tree checks.2module 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		// Sync may retain an event for an off-line save this receiver never fetched.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}