UsmIgnore.vala 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. using Invercargill.DataStructures;
  2. namespace Usm {
  3. /**
  4. * Matcher for a package root's `.usmignore` file, consulted by
  5. * `usm manifest package` to prune files and directories from the
  6. * produced archive.
  7. *
  8. * Semantics (there is no negation: `!` is a literal pattern character,
  9. * it cannot re-include a previously ignored path):
  10. *
  11. * - Blank lines and lines starting with `#` are skipped.
  12. * - A pattern containing `/` matches the FULL path relative to the
  13. * package root; a pattern without `/` matches any path suffix
  14. * (equivalently, the basename at any depth).
  15. * - A trailing `/` marks a directory-only pattern: it matches the
  16. * directory itself and everything beneath it.
  17. * - `*` and `?` are wildcards and never match `/`.
  18. * - The root `.usmignore` and `MANIFEST.usm` files can never be ignored.
  19. * - The `.git` directory is always ignored, with or without a
  20. * `.usmignore` file; when no `.usmignore` exists it is the only
  21. * default ignore.
  22. *
  23. * ```
  24. * # .usmignore
  25. * builddir/
  26. * *.sqlite
  27. * docs/generated/
  28. * ```
  29. */
  30. public class UsmIgnore : Object {
  31. private Vector<UsmIgnore.Pattern> patterns = new Vector<UsmIgnore.Pattern>();
  32. /**
  33. * Creates the default matcher: only the `.git` directory (and
  34. * everything beneath it) is ignored.
  35. */
  36. public UsmIgnore() {
  37. patterns.add(new UsmIgnore.Pattern(".git", true));
  38. }
  39. /**
  40. * Loads the `.usmignore` file at the root of {@link root_path},
  41. * falling back to the {@link UsmIgnore} defaults when the file is
  42. * absent.
  43. *
  44. * Throws when the file exists but cannot be read.
  45. */
  46. public UsmIgnore.from_root(string root_path) throws Error {
  47. this();
  48. var path = Path.build_filename(root_path, ".usmignore");
  49. if(!FileUtils.test(path, FileTest.EXISTS)) {
  50. return;
  51. }
  52. string contents;
  53. FileUtils.get_contents(path, out contents);
  54. foreach(var line in contents.split("\n")) {
  55. add_pattern(line);
  56. }
  57. }
  58. /**
  59. * Adds one pattern line; blank lines and `#` comments are skipped.
  60. */
  61. public void add_pattern(string line) {
  62. var trimmed = line.strip();
  63. if(trimmed.length == 0 || trimmed.has_prefix("#")) {
  64. return;
  65. }
  66. var directory_only = trimmed.has_suffix("/");
  67. var source = directory_only ? trimmed.substring(0, trimmed.length - 1) : trimmed;
  68. if(source.length == 0) {
  69. return;
  70. }
  71. patterns.add(new UsmIgnore.Pattern(source, directory_only));
  72. }
  73. /**
  74. * Whether the file or directory at {@link relative_path} (relative
  75. * to the package root, without a leading `./` or `/`) is ignored.
  76. */
  77. public bool matches(string relative_path, bool is_directory) {
  78. var path = relative_path.has_prefix("./") ? relative_path.substring(2) : relative_path;
  79. if(path == ".usmignore" || path == "MANIFEST.usm") {
  80. return false;
  81. }
  82. foreach(var pattern in patterns) {
  83. if(pattern.matches(path, is_directory)) {
  84. return true;
  85. }
  86. }
  87. return false;
  88. }
  89. /**
  90. * Classic two-pointer wildcard match where `*` and `?` never match
  91. * the path separator.
  92. */
  93. private static bool glob_matches(string pattern, string text) {
  94. int pattern_index = 0;
  95. int text_index = 0;
  96. int star_index = -1;
  97. int backtrack_index = 0;
  98. while(text_index < text.length) {
  99. char pattern_char = pattern_index < pattern.length ? pattern[pattern_index] : '\0';
  100. char text_char = text[text_index];
  101. if((pattern_char == '?' && text_char != '/') || pattern_char == text_char) {
  102. pattern_index++;
  103. text_index++;
  104. }
  105. else if(pattern_char == '*') {
  106. star_index = pattern_index++;
  107. backtrack_index = text_index;
  108. }
  109. else if(star_index >= 0) {
  110. pattern_index = star_index + 1;
  111. text_index = ++backtrack_index;
  112. }
  113. else {
  114. return false;
  115. }
  116. }
  117. while(pattern_index < pattern.length && pattern[pattern_index] == '*') {
  118. pattern_index++;
  119. }
  120. return pattern_index == pattern.length;
  121. }
  122. /**
  123. * One compiled `.usmignore` line. A pattern whose source contains
  124. * `/` is anchored to the full relative path, otherwise it matches
  125. * any single path segment suffix.
  126. */
  127. private class Pattern : Object {
  128. public string source;
  129. public bool directory_only;
  130. public bool anchored {
  131. get {
  132. return source.contains("/");
  133. }
  134. }
  135. public Pattern(string source, bool directory_only) {
  136. this.source = source;
  137. this.directory_only = directory_only;
  138. }
  139. public bool matches(string path, bool is_directory) {
  140. if(directory_only) {
  141. if(is_directory && segment_matches(path)) {
  142. return true;
  143. }
  144. return matches_beneath(path);
  145. }
  146. if(anchored) {
  147. return UsmIgnore.glob_matches(source, path);
  148. }
  149. return UsmIgnore.glob_matches(source, Path.get_basename(path));
  150. }
  151. private bool segment_matches(string path) {
  152. return anchored ? UsmIgnore.glob_matches(source, path)
  153. : UsmIgnore.glob_matches(source, Path.get_basename(path));
  154. }
  155. /**
  156. * Everything beneath a directory matching a directory-only
  157. * pattern is ignored, whether or not the walker pruned the
  158. * directory itself.
  159. */
  160. private bool matches_beneath(string path) {
  161. var separator = path.index_of("/");
  162. while(separator != -1) {
  163. if(segment_matches(path.substring(0, separator))) {
  164. return true;
  165. }
  166. separator = path.index_of("/", separator + 1);
  167. }
  168. return false;
  169. }
  170. }
  171. }
  172. }