ignore.v

4.7 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.102//103// LOG.md is rewritten after every save from the history itself. Tracking it104// would make the tree dirty the moment a save lands, so `status` could never105// report a clean tree and every diff would carry the log's own churn.106pub fn hard_ignored(rel string) bool {107	if rel == '.koh' || rel == '.koh-wal' || rel == '.koh-shm' {108		return true109	}110	if rel == '.koh-v2' || rel.starts_with('.koh-v2/') {111		return true112	}113	if rel == '.git' || rel.starts_with('.git/') {114		return true115	}116	if rel == 'LOG.md' {117		return true118	}119	return false120}121122pub fn (ig Ignore) matches(rel string) bool {123	if hard_ignored(rel) {124		return true125	}126	parts := rel.split('/')127	for pat in ig.patterns {128		if ig.matches_pattern(parts, pat) {129			return true130		}131	}132	return false133}134135fn (ig Ignore) matches_pattern(parts []string, pat Pattern) bool {136	if pat.anchored {137		// A directory pattern matches any path below that directory.138		if pat.dir_only {139			return parts.len >= 2 && match_segments(parts, 0, pat.segments, 0, true)140		}141		return match_segments(parts, 0, pat.segments, 0, false)142	}143	if pat.dir_only {144		for start in 0 .. parts.len {145			if parts.len - start >= 2 && match_segments(parts, start, pat.segments, 0, true) {146				return true147			}148		}149		return false150	}151	for start in 0 .. parts.len {152		if match_segments(parts, start, pat.segments, 0, false) {153			return true154		}155	}156	return false157}158159// match_segments consumes `pat` from `gi` over `path` from `pi`.160// `allow_trailing` lets the path keep segments the pattern does not name,161// which is what makes a directory pattern cover everything beneath it.162fn match_segments(path []string, pi int, pat []string, gi int, allow_trailing bool) bool {163	if gi == pat.len {164		return allow_trailing || pi == path.len165	}166	seg := pat[gi]167	if seg == '**' {168		for skip in pi .. path.len + 1 {169			if match_segments(path, skip, pat, gi + 1, allow_trailing) {170				return true171			}172		}173		return false174	}175	if pi >= path.len {176		return false177	}178	if !match_segment(path[pi], seg) {179		return false180	}181	return match_segments(path, pi + 1, pat, gi + 1, allow_trailing)182}183184// match_segment is shell-style matching within one path segment.185fn match_segment(name string, pat string) bool {186	mut n := 0187	mut p := 0188	mut star := -1189	mut mark := 0190	for n < name.len {191		if p < pat.len && (pat[p] == name[n] || pat[p] == `?`) {192			n++193			p++194		} else if p < pat.len && pat[p] == `*` {195			star = p196			mark = n197			p++198		} else if star >= 0 {199			p = star + 1200			mark++201			n = mark202		} else {203			return false204		}205	}206	for p < pat.len && pat[p] == `*` {207		p++208	}209	return p == pat.len210}