ignore.v

5.3 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}6768pub fn parse(text string) Ignore {69	mut patterns := []Pattern{}70	for raw in text.split_into_lines() {71		line := raw.trim_space()72		if line == '' || line.starts_with('#') {73			continue74		}75		mut body := line76		mut dir_only := false77		if body.ends_with('/') {78			dir_only = true79			body = body.trim_right('/')80		}81		mut anchored := false82		if body.starts_with('/') {83			anchored = true84			body = body.trim_left('/')85		}86		if body == '' {87			continue88		}89		patterns << Pattern{90			segments: body.split('/')91			dir_only: dir_only92			anchored: anchored93		}94	}95	return Ignore{96		patterns: patterns97	}98}99100// hard_ignored covers the files Koh writes into a repo as part of working101// there. They are never snapshotted whatever `.kohignore` says, at any depth:102// a repo inside the repo keeps its own history, and restoring an old copy of103// its `.koh` or `.git` would roll that history back.104//105// LOG.md is rewritten after every save from the history itself. Tracking it106// would make the tree dirty the moment a save lands, so `status` could never107// report a clean tree and every diff would carry the log's own churn.108pub fn hard_ignored(rel string) bool {109	if rel == 'LOG.md' {110		return true111	}112	return rel.split('/').any(it in hard_ignored_names)113}114115const hard_ignored_names = ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.git']116117// matches reports whether a repo-relative path is ignored. `is_dir` matters118// because a pattern ending in `/` names a directory: it covers the directory119// itself and everything below it, but not a file that happens to share the120// name.121pub fn (ig Ignore) matches(rel string, is_dir bool) bool {122	if hard_ignored(rel) {123		return true124	}125	parts := rel.split('/')126	for pat in ig.patterns {127		if ig.matches_pattern(parts, is_dir, pat) {128			return true129		}130	}131	return false132}133134fn (ig Ignore) matches_pattern(parts []string, is_dir bool, pat Pattern) bool {135	if pat.dir_only {136		// Anything inside an ignored directory is ignored too.137		if parts.len > 1 && match_any_prefix(parts[..parts.len - 1], pat.segments, pat.anchored) {138			return true139		}140		if !is_dir {141			return false142		}143	}144	// Otherwise the pattern has to match the whole path.145	for start in 0 .. parts.len {146		if pat.anchored && start != 0 {147			break148		}149		if match_segments(parts, start, pat.segments, 0, false) {150			return true151		}152	}153	return false154}155156// match_any_prefix asks whether the pattern matches a leading run of segments157// starting at any point in the path.158fn match_any_prefix(parts []string, pat []string, anchored bool) bool {159	for start in 0 .. parts.len {160		if anchored && start != 0 {161			break162		}163		for end in start + 1 .. parts.len + 1 {164			if match_segments(parts[start..end], 0, pat, 0, false) {165				return true166			}167		}168	}169	return false170}171172// match_segments consumes `pat` from `gi` over `path` from `pi`.173// `allow_trailing` lets the path keep segments the pattern does not name,174// which is what makes a directory pattern cover everything beneath it.175fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {176	if gi == pat.len {177		return allow_trailing || pi == path.len178	}179	seg := pat[gi]180	if seg == '**' {181		for skip in pi .. path.len + 1 {182			if match_segments(path, skip, pat, gi + 1, allow_trailing) {183				return true184			}185		}186		return false187	}188	if pi >= path.len {189		return false190	}191	if !match_segment(path[pi], seg) {192		return false193	}194	return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)195}196197// match_segment is shell-style matching within one path segment.198fn match_segment(name string, pat string) bool {199	mut n := 0200	mut p := 0201	mut star := -1202	mut mark := 0203	for n < name.len {204		if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {205			n++206			p++207		} else if p < pat.len && pat[p] == `*` {208			star = p209			mark = n210			p++211		} else if star >= 0 {212			p = star + 1213			mark++214			n = mark215		} else {216			return false217		}218	}219	for p < pat.len && pat[p] == `*` {220		p++221	}222	return p == pat.len223}