pub use isoloom_manifest::ignore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Copy {
Files,
Archive,
Runner,
}
const RULES: &str = r##"
# The project's files the machines don't get: version control and the tools' state, and what
# .isoloomignore at the project's root lists (gitignore-style: one pattern per line, `#` comments,
# `!` brings a path back, a trailing `/` matches folders only, a `/` at the start or in the middle
# anchors the pattern at the root; `*`, `?`, `[abc]`, `**`; the last pattern matching a path, or
# a folder it is in, decides). Read as regular expressions, as the Vagrantfiles do.
locals {
project_ignore_lines = [for l in split("\n", replace(try(file("${local.root}/.isoloomignore"), ""), "\r", "")) : trimspace(l)]
project_ignore_pats = [for l in local.project_ignore_lines : { keep = startswith(l, "!"), pat = trimprefix(l, "!") } if l != "" && !startswith(l, "#")]
project_ignore_globs = [for r in local.project_ignore_pats : merge(r, { glob = trimsuffix(trimsuffix(trimsuffix(r.pat, "/"), "/**"), "/") })]
project_ignore = [for r in local.project_ignore_globs : {
keep = r.keep
re = format(r.glob == r.pat ? "^%s(/|$)" : "^%s/", replace(replace(replace(replace(replace(replace(replace(replace(
length(split("/", r.glob)) > 1 ? trimprefix(r.glob, "/") : "**/${r.glob}",
"/[.+^$(){}|\\\\]/", "\\$0"), "[!", "[^"), "**/", "\u0001"), "**", "\u0002"), "*", "[^/]*"), "?", "[^/]"), "\u0001", "(.*/)?"), "\u0002", ".*"))
} if r.glob != ""]
# Each file: whether it goes (the last matching pattern decides; none: it goes).
"##;
const PATHS: (&str, &str) = (
"project_paths",
r#"[for f, keep in local.project_keep : f if keep && length(regexall("SKIP", f)) == 0]"#,
);
const IGNORED: (&str, &str) = ("project_ignored", "[for f, keep in local.project_keep : f if !keep]");
const KEEP: (&str, &str) = (
"project_keep",
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] }"#,
);
const EXCLUSIONS: &str = r#"
# What `tar` leaves out of the project's archive: .isoloomignore's files, from the archive's top
# folder, their wildcard characters escaped.
resource "local_file" "isoloom_project" {
filename = "${path.module}/.isoloom-project-ignored.txt"
content = join("", [for f in local.project_ignored : "${replace("${basename(local.root)}/${f}", "/[\\\\*?\\[]/", "\\$0")}\n"])
}
"#;
const SKIP: &str = r"^\\.isoloom|(^|/)\\.(git|vagrant|terraform)/";
#[must_use]
pub fn lists(copy: Copy, module: &str) -> String {
if copy == Copy::Runner {
return String::new();
}
let mut listed = Vec::new();
if module.contains("local.project_paths") {
listed.push(PATHS);
}
if copy == Copy::Archive {
listed.push(IGNORED);
}
if listed.is_empty() {
return String::new();
}
listed.insert(0, KEEP);
let width = listed.iter().map(|(name, _)| name.len()).max().unwrap_or_default();
let lines = listed
.into_iter()
.map(|(name, value)| format!(" {name:<width$} = {}\n", value.replace("SKIP", SKIP)))
.collect::<Vec<_>>()
.concat();
let exclusions = if copy == Copy::Archive { EXCLUSIONS } else { "" };
format!("{RULES}{lines}}}\n{exclusions}")
}
#[cfg(test)]
mod tests {
use super::{Copy, lists};
const READS_PATHS: &str = "content = join(\"\\n\", local.project_paths)";
#[test]
fn the_files_alone_list_only_the_files_that_go_when_the_module_reads_them() {
let files = lists(Copy::Files, READS_PATHS);
assert!(files.contains(" project_keep = { for f in fileset(local.root"), "{files}");
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}");
assert!(!files.contains("project_ignored") && !files.contains("local_file"), "{files}");
}
#[test]
fn a_module_reading_no_list_gets_none() {
assert_eq!(lists(Copy::Files, "resource \"x\" \"y\" {}"), "");
assert_eq!(lists(Copy::Runner, READS_PATHS), "");
}
#[test]
fn an_archive_lists_what_tar_leaves_out_and_the_files_that_go_only_when_read() {
let archive = lists(Copy::Archive, "");
assert!(
archive.contains(" project_keep = {") && archive.contains(" project_ignored = [for f, keep"),
"{archive}"
);
assert!(!archive.contains("project_paths"), "{archive}");
assert!(
archive.ends_with("\\n\"])\n}\n") && archive.contains("resource \"local_file\" \"isoloom_project\""),
"{archive}"
);
let both = lists(Copy::Archive, READS_PATHS);
assert!(
both.contains(" project_paths = [for f") && both.contains(" project_ignored = [for f"),
"{both}"
);
}
}