| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- 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<UsmIgnore.Pattern> patterns = new Vector<UsmIgnore.Pattern>();
- /**
- * 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;
- }
- }
- }
- }
|