using Invercargill.DataStructures; namespace Usm { /** * Matcher for a package root's `.usmignore` file, consulted by * `usm manifest package` to prune files and directories from the * produced archive. * * Semantics (there is no negation: `!` is a literal pattern character, * it cannot re-include a previously ignored path): * * - Blank lines and lines starting with `#` are skipped. * - A pattern containing `/` matches the FULL path relative to the * package root; a pattern without `/` matches any path suffix * (equivalently, the basename at any depth). * - A trailing `/` marks a directory-only pattern: it matches the * directory itself and everything beneath it. * - `*` and `?` are wildcards and never match `/`. * - The root `.usmignore` and `MANIFEST.usm` files can never be ignored. * - The `.git` directory is always ignored, with or without a * `.usmignore` file; when no `.usmignore` exists it is the only * default ignore. * * ``` * # .usmignore * builddir/ * *.sqlite * docs/generated/ * ``` */ public class UsmIgnore : Object { private Vector patterns = new Vector(); /** * Creates the default matcher: only the `.git` directory (and * everything beneath it) is ignored. */ public UsmIgnore() { patterns.add(new UsmIgnore.Pattern(".git", true)); } /** * Loads the `.usmignore` file at the root of {@link root_path}, * falling back to the {@link UsmIgnore} defaults when the file is * absent. * * Throws when the file exists but cannot be read. */ public UsmIgnore.from_root(string root_path) throws Error { this(); var path = Path.build_filename(root_path, ".usmignore"); if(!FileUtils.test(path, FileTest.EXISTS)) { return; } string contents; FileUtils.get_contents(path, out contents); foreach(var line in contents.split("\n")) { add_pattern(line); } } /** * Adds one pattern line; blank lines and `#` comments are skipped. */ public void add_pattern(string line) { var trimmed = line.strip(); if(trimmed.length == 0 || trimmed.has_prefix("#")) { return; } var directory_only = trimmed.has_suffix("/"); var source = directory_only ? trimmed.substring(0, trimmed.length - 1) : trimmed; if(source.length == 0) { return; } patterns.add(new UsmIgnore.Pattern(source, directory_only)); } /** * Whether the file or directory at {@link relative_path} (relative * to the package root, without a leading `./` or `/`) is ignored. */ public bool matches(string relative_path, bool is_directory) { var path = relative_path.has_prefix("./") ? relative_path.substring(2) : relative_path; if(path == ".usmignore" || path == "MANIFEST.usm") { return false; } foreach(var pattern in patterns) { if(pattern.matches(path, is_directory)) { return true; } } return false; } /** * Classic two-pointer wildcard match where `*` and `?` never match * the path separator. */ private static bool glob_matches(string pattern, string text) { int pattern_index = 0; int text_index = 0; int star_index = -1; int backtrack_index = 0; while(text_index < text.length) { char pattern_char = pattern_index < pattern.length ? pattern[pattern_index] : '\0'; char text_char = text[text_index]; if((pattern_char == '?' && text_char != '/') || pattern_char == text_char) { pattern_index++; text_index++; } else if(pattern_char == '*') { star_index = pattern_index++; backtrack_index = text_index; } else if(star_index >= 0) { pattern_index = star_index + 1; text_index = ++backtrack_index; } else { return false; } } while(pattern_index < pattern.length && pattern[pattern_index] == '*') { pattern_index++; } return pattern_index == pattern.length; } /** * One compiled `.usmignore` line. A pattern whose source contains * `/` is anchored to the full relative path, otherwise it matches * any single path segment suffix. */ private class Pattern : Object { public string source; public bool directory_only; public bool anchored { get { return source.contains("/"); } } public Pattern(string source, bool directory_only) { this.source = source; this.directory_only = directory_only; } public bool matches(string path, bool is_directory) { if(directory_only) { if(is_directory && segment_matches(path)) { return true; } return matches_beneath(path); } if(anchored) { return UsmIgnore.glob_matches(source, path); } return UsmIgnore.glob_matches(source, Path.get_basename(path)); } private bool segment_matches(string path) { return anchored ? UsmIgnore.glob_matches(source, path) : UsmIgnore.glob_matches(source, Path.get_basename(path)); } /** * Everything beneath a directory matching a directory-only * pattern is ignored, whether or not the walker pruned the * directory itself. */ private bool matches_beneath(string path) { var separator = path.index_of("/"); while(separator != -1) { if(segment_matches(path.substring(0, separator))) { return true; } separator = path.index_of("/", separator + 1); } return false; } } } }