1pub use isoloom_manifest::ignore;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum Copy {
26 Files,
28 Archive,
30 Runner,
33}
34
35const RULES: &str = r##"
38# The project's files the machines don't get: version control and the tools' state, and what
39# .isoloomignore at the project's root lists (gitignore-style: one pattern per line, `#` comments,
40# `!` brings a path back, a trailing `/` matches folders only, a `/` at the start or in the middle
41# anchors the pattern at the root; `*`, `?`, `[abc]`, `**`; the last pattern matching a path, or
42# a folder it is in, decides). Read as regular expressions, as the Vagrantfiles do.
43locals {
44 project_ignore_lines = [for l in split("\n", replace(try(file("${local.root}/.isoloomignore"), ""), "\r", "")) : trimspace(l)]
45 project_ignore_pats = [for l in local.project_ignore_lines : { keep = startswith(l, "!"), pat = trimprefix(l, "!") } if l != "" && !startswith(l, "#")]
46 project_ignore_globs = [for r in local.project_ignore_pats : merge(r, { glob = trimsuffix(trimsuffix(trimsuffix(r.pat, "/"), "/**"), "/") })]
47 project_ignore = [for r in local.project_ignore_globs : {
48 keep = r.keep
49 re = format(r.glob == r.pat ? "^%s(/|$)" : "^%s/", replace(replace(replace(replace(replace(replace(replace(replace(
50 length(split("/", r.glob)) > 1 ? trimprefix(r.glob, "/") : "**/${r.glob}",
51 "/[.+^$(){}|\\\\]/", "\\$0"), "[!", "[^"), "**/", "\u0001"), "**", "\u0002"), "*", "[^/]*"), "?", "[^/]"), "\u0001", "(.*/)?"), "\u0002", ".*"))
52 } if r.glob != ""]
53 # Each file: whether it goes (the last matching pattern decides; none: it goes).
54"##;
55
56const PATHS: (&str, &str) = (
59 "project_paths",
60 r#"[for f, keep in local.project_keep : f if keep && length(regexall("SKIP", f)) == 0]"#,
61);
62
63const IGNORED: (&str, &str) = ("project_ignored", "[for f, keep in local.project_keep : f if !keep]");
65
66const KEEP: (&str, &str) = (
68 "project_keep",
69 r#"{ for f in fileset(local.root, "**") : f => reverse(concat([true], [for r in local.project_ignore : r.keep if length(regexall(r.re, f)) > 0]))[0] }"#,
70);
71
72const EXCLUSIONS: &str = r#"
73# What `tar` leaves out of the project's archive: .isoloomignore's files, from the archive's top
74# folder, their wildcard characters escaped.
75resource "local_file" "isoloom_project" {
76 filename = "${path.module}/.isoloom-project-ignored.txt"
77 content = join("", [for f in local.project_ignored : "${replace("${basename(local.root)}/${f}", "/[\\\\*?\\[]/", "\\$0")}\n"])
78}
79"#;
80
81const SKIP: &str = r"^\\.isoloom|(^|/)\\.(git|vagrant|terraform)/";
84
85#[must_use]
90pub fn lists(copy: Copy, module: &str) -> String {
91 if copy == Copy::Runner {
92 return String::new();
93 }
94 let mut listed = Vec::new();
95 if module.contains("local.project_paths") {
96 listed.push(PATHS);
97 }
98 if copy == Copy::Archive {
99 listed.push(IGNORED);
100 }
101 if listed.is_empty() {
102 return String::new();
103 }
104 listed.insert(0, KEEP);
105 let width = listed.iter().map(|(name, _)| name.len()).max().unwrap_or_default();
107 let lines = listed
108 .into_iter()
109 .map(|(name, value)| format!(" {name:<width$} = {}\n", value.replace("SKIP", SKIP)))
110 .collect::<Vec<_>>()
111 .concat();
112 let exclusions = if copy == Copy::Archive { EXCLUSIONS } else { "" };
113 format!("{RULES}{lines}}}\n{exclusions}")
114}
115
116#[cfg(test)]
117mod tests {
118 use super::{Copy, lists};
119
120 const READS_PATHS: &str = "content = join(\"\\n\", local.project_paths)";
122
123 #[test]
124 fn the_files_alone_list_only_the_files_that_go_when_the_module_reads_them() {
125 let files = lists(Copy::Files, READS_PATHS);
126 assert!(files.contains(" project_keep = { for f in fileset(local.root"), "{files}");
127 assert!(files.contains(" project_paths = [for f, keep in local.project_keep : f if keep && length(regexall(\"^\\\\.isoloom|(^|/)\\\\.(git|vagrant|terraform)/\", f)) == 0]\n}\n"), "{files}");
128 assert!(!files.contains("project_ignored") && !files.contains("local_file"), "{files}");
129 }
130
131 #[test]
132 fn a_module_reading_no_list_gets_none() {
133 assert_eq!(lists(Copy::Files, "resource \"x\" \"y\" {}"), "");
134 assert_eq!(lists(Copy::Runner, READS_PATHS), "");
136 }
137
138 #[test]
139 fn an_archive_lists_what_tar_leaves_out_and_the_files_that_go_only_when_read() {
140 let archive = lists(Copy::Archive, "");
141 assert!(
142 archive.contains(" project_keep = {") && archive.contains(" project_ignored = [for f, keep"),
143 "{archive}"
144 );
145 assert!(!archive.contains("project_paths"), "{archive}");
146 assert!(
147 archive.ends_with("\\n\"])\n}\n") && archive.contains("resource \"local_file\" \"isoloom_project\""),
148 "{archive}"
149 );
150 let both = lists(Copy::Archive, READS_PATHS);
151 assert!(
152 both.contains(" project_paths = [for f") && both.contains(" project_ignored = [for f"),
153 "{both}"
154 );
155 }
156}