ignore.v

6.2 KB · raw

1//! Ignore rules.2//!3//! `.kohignore` lives at the repo root and is itself snapshotted, so the rules4//! travel between machines with the history. Patterns are matched against5//! repo-relative paths: `*` matches inside one path segment, `**` spans6//! segments, a trailing `/` restricts a pattern to directories, and a leading7//! `/` anchors it to the repo root.89module snapshot1011import os1213// The seed written by `koh init`, carried over from Koh 2's default list.14pub const default_ignore = '# koh ignore rules (.kohignore)15# Generated by koh init. Edit to fit the project.1617# Patterns are matched against repo-relative paths. Suffix `/` restricts18# to directories. Leading `/` anchors to the repo root. A pattern like19# `*.log` matches any file with that extension anywhere in the tree.20#21# koh already ignores .koh, .koh-wal, .koh-shm, .koh-v2/ and .git/ automatically.2223# Build output24zig-out/25.zig-cache/26zig-cache/27target/28build/29dist/30out/3132# Dependencies33node_modules/3435# OS noise36.DS_Store37Thumbs.db3839# Logs40*.log4142# Koh artifacts43*.bundle44*.koh.offer45'4647pub const ignore_file = '.kohignore'4849struct Pattern {50	segments []string51	dir_only bool52	anchored bool53}5455pub struct Ignore {56	patterns []Pattern57}5859// load reads `.kohignore`; a repo without one simply has no extra rules.60pub fn load(root string) Ignore {61	path := os.join_path(root, ignore_file)62	if !os.is_file(path) {63		return Ignore{}64	}65	return parse(os.read_file(path) or { return Ignore{} })66}6768// banning adds a whole-history strip's paths as anchored rules, so the tree agrees with the69// history on every machine that holds the strip, before `.kohignore` names them there too.70fn (ig Ignore) banning(paths []string) Ignore {71	mut patterns := ig.patterns.clone()72	for path in paths {73		patterns << Pattern{74			segments: path.split('/')75			anchored: true76		}77	}78	return Ignore{79		patterns: patterns80	}81}8283/// add_ignore appends an anchored `/<path>` rule to `.kohignore`, unless that line is already84/// there, and reports whether it wrote one.85pub fn add_ignore(root string, path string) !bool {86	file := os.join_path(root, ignore_file)87	text := if os.is_file(file) { os.read_file(file)! } else { '' }88	rule := '/${path}'89	if text.split_into_lines().any(it.trim_space() == rule) {90		return false91	}92	separator := if text == '' || text.ends_with('\n') { '' } else { '\n' }93	os.write_file(file, '${text}${separator}${rule}\n')!94	return true95}9697pub fn parse(text string) Ignore {98	mut patterns := []Pattern{}99	for raw in text.split_into_lines() {100		line := raw.trim_space()101		if line == '' || line.starts_with('#') {102			continue103		}104		mut body := line105		mut dir_only := false106		if body.ends_with('/') {107			dir_only = true108			body = body.trim_right('/')109		}110		mut anchored := false111		if body.starts_with('/') {112			anchored = true113			body = body.trim_left('/')114		}115		if body == '' {116			continue117		}118		patterns << Pattern{119			segments: body.split('/')120			dir_only: dir_only121			anchored: anchored122		}123	}124	return Ignore{125		patterns: patterns126	}127}128129// hard_ignored covers the files Koh writes into a repo as part of working130// there. They are never snapshotted whatever `.kohignore` says, at any depth:131// a repo inside the repo keeps its own history, and restoring an old copy of132// its `.koh` or `.git` would roll that history back.133//134// LOG.md is rewritten after every save from the history itself. Tracking it135// would make the tree dirty the moment a save lands, so `status` could never136// report a clean tree and every diff would carry the log's own churn.137pub fn hard_ignored(rel string) bool {138	if rel == 'LOG.md' {139		return true140	}141	return rel.split('/').any(it in hard_ignored_names)142}143144const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']145146// matches reports whether a repo-relative path is ignored. `is_dir` matters147// because a pattern ending in `/` names a directory: it covers the directory148// itself and everything below it, but not a file that happens to share the149// name.150pub fn (ig Ignore) matches(rel string, is_dir bool) bool {151	if hard_ignored(rel) {152		return true153	}154	parts := rel.split('/')155	for pat in ig.patterns {156		if ig.matches_pattern(parts, is_dir, pat) {157			return true158		}159	}160	return false161}162163fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {164	if pat.dir_only {165		// Anything inside an ignored directory is ignored too.166		if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {167			return true168		}169		if !is_dir {170			return false171		}172	}173	// Otherwise the pattern has to match the whole path.174	for start in 0 .. parts.len {175		if pat.anchored && start != 0 {176			break177		}178		if match_segments(parts, start, pat.segments, 0, false) {179			return true180		}181	}182	return false183}184185// match_any_prefix asks whether the pattern matches a leading run of segments186// starting at any point in the path.187fn match_any_prefix(parts []string, pat []string, anchored bool) bool {188	for start in 0 .. parts.len {189		if anchored && start != 0 {190			break191		}192		for end in start + 1 .. parts.len + 1 {193			if match_segments(parts[start..end], 0, pat, 0, false) {194				return true195			}196		}197	}198	return false199}200201// match_segments consumes `pat` from `gi` over `path` from `pi`.202// `allow_trailing` lets the path keep segments the pattern does not name,203// which is what makes a directory pattern cover everything beneath it.204fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {205	if gi == pat.len {206		return allow_trailing || pi == path.len207	}208	seg := pat[gi]209	if seg == '**' {210		for skip in pi .. path.len + 1 {211			if match_segments(path, skip, pat, gi + 1, allow_trailing) {212				return true213			}214		}215		return false216	}217	if pi >= path.len {218		return false219	}220	if !match_segment(path[pi], seg) {221		return false222	}223	return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)224}225226// match_segment is shell-style matching within one path segment.227fn match_segment(name string, pat string) bool {228	mut n := 0229	mut p := 0230	mut star := -1231	mut mark := 0232	for n < name.len {233		if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {234			n++235			p++236		} else if p < pat.len && pat[p] == `*` {237			star = p238			mark = n239			p++240		} else if star >= 0 {241			p = star + 1242			mark++243			n = mark244		} else {245			return false246		}247	}248	for p < pat.len && pat[p] == `*` {249		p++250	}251	return p == pat.len252}