use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::manifest::CONFIG_DIR;
const CONFIG_NAME: &str = "config.yaml";
const CACHE_GITIGNORE: &str =
"# Throwaway mmz cache; the config beside it stays tracked.\n/cache/\n";
pub const TEMPLATE: &str = concat!(
"# yaml-language-server: $schema=https://raw.githubusercontent.com/mlavrinenko/mmz/v",
env!("CARGO_PKG_VERSION"),
"/schema/mmz.schema.json
# .mmz/config.yaml — memoized command runner config.
# Prefix a command with `mmz`; it is skipped when the matched rule's inputs are
# byte-for-byte unchanged since the command last succeeded.
# Named glob sets, declared once and referenced by commands. `*` stays within a
# directory; `**` crosses directories.
scopes:
rust: [\"**/*.rs\", \"Cargo.toml\", \"Cargo.lock\"]
# Ordered rules. The first whose name is a token-prefix of the command wins.
# Set `match: exact` on a rule to match only the bare command (no extra args).
commands:
- name: cargo test
inputs: [rust]
# A single {scope} macro in a name fans the rule over that scope's files: one
# per-file cache record, each keyed by and scoped to its own file plus the
# shared `inputs` pins. Best for commands that read only the one file (a
# per-file lint/format); a whole-crate command under-declares this way.
# scopes:
# lint-targets: [\"src/**/*.rs\"]
# commands:
# - name: \"ruff check {lint-targets}\"
# Optional labels on a rule. `mmz --is-fresh --tag <tag>` (and
# `mmz --status --tag <tag>`) then gates only the rules carrying every listed
# tag, instead of every rule in the manifest — so one manifest can hold both
# a gating subset (wired into a pre-push hook or `just check`) and other
# memoized commands (e.g. bench) that a gate should ignore. Untagged rules
# never match a --tag filter; repeat --tag to require more than one.
# commands:
# - name: cargo test
# inputs: [rust]
# tags: [gate]
# - name: cargo bench
# inputs: [rust]
# tags: [bench]
# Printed to stderr when a command is skipped. `{cache:<field>}` pulls a field
# straight from the cache record (command, ran_at, input_digest, ...). Set it per
# command to override, or to \"\" to silence that one.
on_hit: \"mmz: skipped {cache:command} (inputs unchanged)\"
# Directory for throwaway cache records, relative to the project root (the
# directory holding .mmz). The generated .mmz/.gitignore already ignores it.
# cache_dir: .mmz/cache
# Skip git-ignored paths when expanding globs (default true).
# gitignore: true
# Runtime cases mmz errors on instead of falling back. Omit for all (the safe
# default); list a subset to relax the rest; use [] to fall back everywhere.
# strict: [no_match, no_inputs]
"
);
pub fn init(cwd: &Path) -> Result<PathBuf> {
let dir = cwd.join(CONFIG_DIR);
let path = dir.join(CONFIG_NAME);
if path.exists() {
return Err(Error::ManifestExists { path });
}
std::fs::create_dir_all(&dir)?;
let ignore = dir.join(".gitignore");
if !ignore.exists() {
std::fs::write(&ignore, CACHE_GITIGNORE)?;
}
std::fs::write(&path, TEMPLATE)?;
Ok(path)
}
#[cfg(test)]
mod tests {
use super::{CACHE_GITIGNORE, CONFIG_NAME, TEMPLATE, init};
use crate::manifest::{CONFIG_DIR, Manifest};
#[test]
fn template_is_a_valid_manifest() {
let manifest: Manifest = serde_yaml_ng::from_str(TEMPLATE).expect("template parses");
manifest.validate().expect("template validates");
}
#[test]
fn template_pins_schema_to_this_version() {
let pinned = format!("mmz/v{}/schema/mmz.schema.json", env!("CARGO_PKG_VERSION"));
assert!(
TEMPLATE.contains(&pinned),
"schema URL pins the build version"
);
assert!(
!TEMPLATE.contains("/main/"),
"no floating main ref in the scaffolded schema URL"
);
}
#[test]
fn template_scaffolds_a_configured_on_hit() {
let manifest: Manifest = serde_yaml_ng::from_str(TEMPLATE).expect("template parses");
assert_eq!(
manifest.on_hit.as_deref(),
Some("mmz: skipped {cache:command} (inputs unchanged)"),
"init ships an on_hit notice using a cache macro"
);
}
#[test]
fn writes_then_refuses_to_clobber() {
let dir = tempfile::tempdir().expect("tempdir");
let path = init(dir.path()).expect("first init writes");
assert_eq!(path, dir.path().join(CONFIG_DIR).join(CONFIG_NAME));
assert!(path.is_file(), "config written");
assert!(init(dir.path()).is_err(), "second init refuses to clobber");
}
#[test]
fn writes_a_gitignore_that_covers_the_cache() {
let dir = tempfile::tempdir().expect("tempdir");
init(dir.path()).expect("init writes");
let ignore = dir.path().join(CONFIG_DIR).join(".gitignore");
let body = std::fs::read_to_string(&ignore).expect("gitignore written");
assert_eq!(body, CACHE_GITIGNORE, "ignores the cache subtree");
assert!(body.contains("/cache/"), "the default cache_dir is covered");
}
}