pub mod evidence;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::embedded;
use crate::error::RkError;
use crate::landing::{Params, Workflow};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectionInput {
pub params: Params,
pub evidence: TargetEvidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TargetEvidence {
pub documents: BTreeMap<String, Vec<u8>>,
pub crate_shape: CrateShape,
pub flake_nix_present: bool,
pub flake_lock_present: bool,
pub flake_recorded: bool,
}
impl TargetEvidence {
#[must_use]
pub fn document(&self, destination: &str) -> Option<&[u8]> {
self.documents.get(destination).map(Vec::as_slice)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CrateShape {
pub cargo_toml: Option<String>,
pub cargo_lock: bool,
pub main_rs: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Projection {
pub candidates: Vec<Candidate>,
pub omissions: Vec<Omission>,
pub collisions: Vec<Collision>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub destination: String,
pub kind: Kind,
pub placement: Placement,
pub bytes: Vec<u8>,
pub region: Option<Vec<u8>>,
pub sources: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placement {
Whole,
Region {
begin: &'static str,
end: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Omission {
pub destination: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Collision {
pub destination: String,
pub reason: String,
}
impl Projection {
pub fn compute(input: &ProjectionInput) -> Result<Self, RkError> {
Self::compute_over(&embedded_snippets(), input)
}
fn compute_over(files: &[(String, &[u8])], input: &ProjectionInput) -> Result<Self, RkError> {
let params = &input.params;
let evidence = &input.evidence;
let mut candidates = Vec::new();
for selected in select_pair(files, params.tech(), params.forge())? {
if !params.nix() && NIX_DESTINATIONS.contains(&selected.destination.as_str()) {
continue;
}
let kind = kind_of(&selected.destination).ok_or_else(|| {
anyhow::anyhow!(
"the embedded sources do not classify {}; the kind table is stale",
selected.destination
)
})?;
let bytes = match kind {
Kind::Rendered => render(selected.bytes, params),
Kind::Seeded | Kind::State => selected.bytes.to_vec(),
};
candidates.push(Candidate {
destination: selected.destination,
kind,
placement: Placement::Whole,
bytes,
region: None,
sources: vec![selected.source.to_owned()],
});
}
let mut collisions = Vec::new();
for destination in BLOCK_DESTINATIONS {
let (template, sources) = block_template(destination, params.workflow())?;
if let Some(whole) = candidates
.iter()
.find(|candidate| candidate.destination == destination)
{
return Err(anyhow::anyhow!(
"{destination} is both a whole file from {} and a marked region from {}; the embedded sources are defective",
whole.sources.join(", "),
sources.join(", ")
)
.into());
}
let region = render(template.as_bytes(), params);
let (begin, end) = block_markers(destination).ok_or_else(|| {
anyhow::anyhow!("{destination} is a block destination with no markers")
})?;
match propose_document(destination, evidence.document(destination), ®ion) {
Ok(bytes) => candidates.push(Candidate {
destination: destination.to_owned(),
kind: Kind::Rendered,
placement: Placement::Region { begin, end },
bytes,
region: Some(region),
sources,
}),
Err(reason) => collisions.push(Collision {
destination: destination.to_owned(),
reason,
}),
}
}
let mut omissions = Vec::new();
if let Some((set, reason)) = nix_withholding(params.nix(), evidence) {
candidates.retain(|candidate| {
if set.contains(&candidate.destination.as_str()) {
omissions.push(Omission {
destination: candidate.destination.clone(),
reason: reason.clone(),
});
false
} else {
true
}
});
}
candidates.sort_by(|a, b| a.destination.cmp(&b.destination));
omissions.sort_by(|a, b| a.destination.cmp(&b.destination));
Ok(Self {
candidates,
omissions,
collisions,
})
}
}
fn embedded_snippets() -> Vec<(String, &'static [u8])> {
embedded::walk(&embedded::SNIPPETS)
.into_iter()
.map(|(path, bytes)| (format!("snippets/{path}"), bytes))
.collect()
}
pub fn check_pair(tech: &str, forge: &str) -> Result<(), RkError> {
select_pair(&embedded_snippets(), tech, forge).map(|_| ())
}
#[must_use]
pub fn supported_pairs() -> Vec<(String, String)> {
let mut pairs = Vec::new();
for (path, _) in embedded_snippets() {
let Some(rest) = path.strip_prefix("snippets/") else {
continue;
};
let mut segments = rest.split('/');
let (Some(tech), Some(forge), Some(_)) =
(segments.next(), segments.next(), segments.next())
else {
continue;
};
if tech.starts_with('_') {
continue;
}
let pair = (tech.to_owned(), forge.to_owned());
if !pairs.contains(&pair) {
pairs.push(pair);
}
}
pairs
}
#[derive(Debug)]
pub struct Selected<'a, T> {
pub destination: String,
pub source: &'a str,
pub bytes: &'a T,
}
pub fn select_pair<'a, T>(
files: &'a [(String, T)],
tech: &str,
forge: &str,
) -> Result<Vec<Selected<'a, T>>, RkError> {
let mut techs: Vec<&str> = Vec::new();
for (path, _) in files {
if let Some(rest) = path.strip_prefix("snippets/")
&& let Some((dir, _)) = rest.split_once('/')
&& !dir.starts_with('_')
&& !techs.contains(&dir)
{
techs.push(dir);
}
}
if tech.starts_with('_') || !techs.contains(&tech) {
return Err(RkError::Usage(format!(
"unknown tech '{tech}'; the bindings are: {}",
techs.join(", ")
)));
}
let pair = format!("snippets/{tech}/{forge}/");
if !files.iter().any(|(path, _)| path.starts_with(&pair)) {
let mut known: Vec<String> = Vec::new();
for tech in &techs {
let prefix = format!("snippets/{tech}/");
for (path, _) in files {
if let Some(rest) = path.strip_prefix(&prefix)
&& let Some((forge, _)) = rest.split_once('/')
{
let entry = format!("{tech}, {forge}");
if !known.contains(&entry) {
known.push(entry);
}
}
}
}
return Err(RkError::Usage(format!(
"the pair ({tech}, {forge}) has no landable files; the supported pairs are: {}",
known.join("; ")
)));
}
let shared = format!("snippets/_shared/{forge}/");
let mut out: Vec<Selected<'a, T>> = Vec::new();
for zone in [&shared, &pair] {
for (path, bytes) in files {
let Some(rel) = path.strip_prefix(zone.as_str()) else {
continue;
};
if let Some(existing) = out.iter().find(|selected| selected.destination == rel) {
return Err(anyhow::anyhow!(
"the shared zone and the pair ({tech}, {forge}) both ship {rel}: {} and {path}; the embedded sources are defective",
existing.source
)
.into());
}
out.push(Selected {
destination: rel.to_owned(),
source: path,
bytes,
});
}
}
Ok(out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
Rendered,
Seeded,
State,
}
impl Kind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Rendered => "rendered",
Self::Seeded => "seeded",
Self::State => "state",
}
}
}
const KINDS: [(&str, Kind); 16] = [
(".github/workflows/release-plz.yml", Kind::Rendered),
(".github/workflows/release-please.yml", Kind::Rendered),
(".github/workflows/release.yml", Kind::Rendered),
(".github/workflows/pr-title.yml", Kind::Rendered),
(".gitlab-ci.yml", Kind::Rendered),
("SECURITY.md", Kind::Rendered),
(".gitlab/ci/mr-title.yml", Kind::Rendered),
("release-plz.toml", Kind::Seeded),
("dist-workspace.toml", Kind::Seeded),
("release-please-config.json", Kind::Seeded),
("cliff.toml", Kind::Seeded),
("nix/package.nix", Kind::Seeded),
("flake.nix", Kind::Seeded),
(".release-please-manifest.json", Kind::State),
("VERSION", Kind::State),
("flake.lock", Kind::State),
];
pub const NIX_DESTINATIONS: [&str; 3] = ["nix/package.nix", "flake.nix", "flake.lock"];
pub const NIX_WITHHOLDABLE: [&str; 2] = ["flake.nix", "flake.lock"];
#[must_use]
pub fn kind_of(destination: &str) -> Option<Kind> {
if BLOCK_DESTINATIONS.contains(&destination) {
return Some(Kind::Rendered);
}
KINDS
.iter()
.find(|(name, _)| *name == destination)
.map(|(_, kind)| *kind)
}
pub fn destinations() -> impl Iterator<Item = &'static str> {
KINDS
.iter()
.map(|(name, _)| *name)
.chain(BLOCK_DESTINATIONS)
}
pub const OWNER_TOKEN: &[u8] = b"OWNER";
pub const REPO_PLACEHOLDER: &str = "OWNER";
pub const REPO_TOKEN: &[u8] = b"RK_REPO";
pub const SCOPE_SHAPE_TOKEN: &[u8] = b"RK_SCOPE_SHAPE";
pub const STYLE_TOKEN: &[u8] = b"RK_STYLE";
pub const TRUNK_BRANCH_TOKEN: &[u8] = b"RK_TRUNK_BRANCH";
pub const LINE_PREFIX_TOKEN: &[u8] = b"RK_LINE_PREFIX";
pub const LINE_PREFIX_RE_TOKEN: &[u8] = b"RK_LINE_PREFIX_RE";
pub const SECURITY_SPANS: [(&[u8], &[u8]); 3] = [
(
b"<!--RK_SECURITY_CONTACT_BEGIN-->",
b"<!--RK_SECURITY_CONTACT_END-->",
),
(
b"<!--RK_SECURITY_RESPONSE_BEGIN-->",
b"<!--RK_SECURITY_RESPONSE_END-->",
),
(
b"<!--RK_SECURITY_DEADLINE_BEGIN-->",
b"<!--RK_SECURITY_DEADLINE_END-->",
),
];
fn acknowledgment(response: &str) -> String {
format!("Maintainers acknowledge a report within {response}.")
}
const DISCLOSURE_ONLY: &[u8] = b"This policy commits to no disclosure deadline.";
fn security_replacements(params: &Params) -> [Option<Vec<u8>>; 3] {
let contact = (!params.security_contact().is_empty())
.then(|| params.security_contact().as_bytes().to_vec());
let promised = params.security_response() != crate::config::RESPONSE_DEFAULT;
[
contact,
promised.then(|| acknowledgment(params.security_response()).into_bytes()),
promised.then(|| DISCLOSURE_ONLY.to_vec()),
]
}
fn replace_span(baseline: &[u8], begin: &[u8], end: &[u8], value: Option<&[u8]>) -> Vec<u8> {
let ordered = find(baseline, begin)
.zip(find(baseline, end))
.filter(|(start, stop)| stop > start);
let Some((start, stop)) = ordered else {
return baseline.to_vec();
};
let mut out = Vec::with_capacity(baseline.len());
out.extend_from_slice(&baseline[..start]);
out.extend_from_slice(value.unwrap_or_else(|| &baseline[start + begin.len()..stop]));
out.extend_from_slice(&baseline[stop + end.len()..]);
out
}
#[must_use]
pub fn render(baseline: &[u8], params: &Params) -> Vec<u8> {
let repo = params.repo();
let owner = repo.split('/').next().unwrap_or(repo);
let mut out = substitute(baseline, OWNER_TOKEN, owner.as_bytes());
if let Some(style) = params.style() {
out = substitute(&out, STYLE_TOKEN, style.as_str().as_bytes());
}
out = substitute(&out, SCOPE_SHAPE_TOKEN, SCOPE_SHAPE.as_bytes());
out = substitute(&out, TRUNK_BRANCH_TOKEN, params.trunk().as_bytes());
let escaped = params.line_prefix().replace('/', "\\/");
out = substitute(&out, LINE_PREFIX_RE_TOKEN, escaped.as_bytes());
out = substitute(&out, LINE_PREFIX_TOKEN, params.line_prefix().as_bytes());
out = substitute(&out, REPO_TOKEN, repo.as_bytes());
for ((begin, end), value) in SECURITY_SPANS.iter().zip(security_replacements(params)) {
out = replace_span(&out, begin, end, value.as_deref());
}
out
}
#[must_use]
pub fn substitute(baseline: &[u8], token: &[u8], value: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(baseline.len());
let mut rest = baseline;
while let Some(at) = find(rest, token) {
out.extend_from_slice(&rest[..at]);
out.extend_from_slice(value);
rest = &rest[at + token.len()..];
}
out.extend_from_slice(rest);
out
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
pub const AGENTS_DESTINATION: &str = "AGENTS.md";
pub const BLOCK_BEGIN: &str = "<!-- BEGIN release-kit -->";
pub const BLOCK_END: &str = "<!-- END release-kit -->";
pub const GLOSSARY_DESTINATION: &str = "GLOSSARY.md";
pub const HOOKS_DESTINATION: &str = ".pre-commit-config.yaml";
pub const BLOCK_DESTINATIONS: [&str; 3] =
[AGENTS_DESTINATION, GLOSSARY_DESTINATION, HOOKS_DESTINATION];
pub const HOOKS_BEGIN: &str = "# BEGIN release-kit";
pub const HOOKS_END: &str = "# END release-kit";
pub const HOOK_TYPES_LINE: &str = "default_install_hook_types: [pre-commit, commit-msg, pre-push]";
pub const AGENTS_BLOCK: &str = "blocks/agents-block.md.in";
pub const GLOSSARY_BLOCK: &str = "blocks/glossary.md.in";
pub const AGENTS_LINE_WORKTREE: &str = "blocks/agents-line-worktree.md.in";
pub const AGENTS_LINE_BRANCHES: &str = "blocks/agents-line-branches.md.in";
pub const PRE_COMMIT_BLOCK: &str = "blocks/pre-commit-block.yaml.in";
pub const PRE_COMMIT_WORKTREE_GUARD: &str = "blocks/pre-commit-worktree-guard.yaml.in";
#[must_use]
pub const fn routing_line(workflow: Workflow) -> &'static str {
match workflow {
Workflow::Worktree => AGENTS_LINE_WORKTREE,
Workflow::Branches => AGENTS_LINE_BRANCHES,
}
}
pub fn embedded_block(path: &str) -> Result<&'static str, RkError> {
let name = path.strip_prefix("blocks/").unwrap_or(path);
let file = embedded::BLOCKS
.get_file(name)
.ok_or_else(|| anyhow::anyhow!("{path}: this binary embeds no such block"))?;
std::str::from_utf8(file.contents())
.map_err(|_| anyhow::anyhow!("{path}: a block is UTF-8").into())
}
#[must_use]
pub fn authored(text: &str) -> &str {
text.strip_suffix('\n').unwrap_or(text)
}
pub const BRANCH_GRAMMAR: &str = r"^((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)/[A-Za-z0-9._/-]+|([0-9]+|[A-Z][A-Z0-9]+-[0-9]+)-[A-Za-z0-9._-]+|release[-/].+)$";
pub const SCOPE_SHAPE: &str = "[a-z0-9._/-]+";
#[must_use]
pub fn scope_is_shaped(scope: &str) -> bool {
!scope.is_empty()
&& scope.chars().all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '/' | '-')
})
}
#[must_use]
pub fn compose_routing(template: &str, line: &str) -> String {
authored(template).replacen("RK_WORKFLOW_LINE", authored(line), 1)
}
#[must_use]
pub fn compose_glossary(template: &str) -> String {
authored(template).to_owned()
}
#[must_use]
pub fn compose_hooks(template: &str, guard: Option<&str>) -> String {
let (guard, skip) = guard.map_or_else(
|| (String::new(), "no-commit-to-branch"),
|entry| {
(
format!("{}\n", authored(entry)),
"no-commit-to-branch,rk-worktree-location",
)
},
);
authored(template)
.replacen("RK_BRANCH_GRAMMAR", BRANCH_GRAMMAR, 1)
.replacen("RK_SWEEP_SKIP", skip, 1)
.replacen("RK_WORKTREE_GUARD", &guard, 1)
}
pub fn routing_block(workflow: Workflow) -> Result<String, RkError> {
Ok(compose_routing(
embedded_block(AGENTS_BLOCK)?,
embedded_block(routing_line(workflow))?,
))
}
pub fn glossary_block() -> Result<String, RkError> {
Ok(compose_glossary(embedded_block(GLOSSARY_BLOCK)?))
}
pub fn hooks_block(workflow: Workflow) -> Result<String, RkError> {
let guard = match workflow {
Workflow::Worktree => Some(embedded_block(PRE_COMMIT_WORKTREE_GUARD)?),
Workflow::Branches => None,
};
Ok(compose_hooks(embedded_block(PRE_COMMIT_BLOCK)?, guard))
}
fn block_template(destination: &str, workflow: Workflow) -> Result<(String, Vec<String>), RkError> {
match destination {
AGENTS_DESTINATION => Ok((
routing_block(workflow)?,
vec![AGENTS_BLOCK.to_owned(), routing_line(workflow).to_owned()],
)),
GLOSSARY_DESTINATION => Ok((glossary_block()?, vec![GLOSSARY_BLOCK.to_owned()])),
HOOKS_DESTINATION => {
let mut sources = vec![PRE_COMMIT_BLOCK.to_owned()];
if workflow == Workflow::Worktree {
sources.push(PRE_COMMIT_WORKTREE_GUARD.to_owned());
}
Ok((hooks_block(workflow)?, sources))
}
other => Err(anyhow::anyhow!("{other} is not a block destination").into()),
}
}
#[must_use]
pub fn block_markers(destination: &str) -> Option<(&'static str, &'static str)> {
match destination {
AGENTS_DESTINATION | GLOSSARY_DESTINATION => Some((BLOCK_BEGIN, BLOCK_END)),
HOOKS_DESTINATION => Some((HOOKS_BEGIN, HOOKS_END)),
_ => None,
}
}
#[must_use]
pub fn extract_block<'a>(text: &'a str, begin: &str, end: &str) -> Option<&'a str> {
let start = text.find(begin)?;
let stop = text[start..].find(end)? + start + end.len();
Some(&text[start..stop])
}
#[must_use]
pub fn splice_marked_block(existing: Option<&[u8]>, block: &str) -> Vec<u8> {
let block = block.as_bytes();
let Some(text) = existing else {
return [block, b"\n"].concat();
};
if let Some(start) = find(text, BLOCK_BEGIN.as_bytes())
&& let Some(offset) = find(&text[start..], BLOCK_END.as_bytes())
{
let stop = start + offset + BLOCK_END.len();
return [&text[..start], block, &text[stop..]].concat();
}
let mut out = Vec::with_capacity(text.len() + block.len() + 3);
out.extend_from_slice(text);
if !text.ends_with(b"\n") {
out.push(b'\n');
}
out.push(b'\n');
out.extend_from_slice(block);
out.push(b'\n');
out
}
pub fn splice_hooks_block(existing: Option<&str>, block: &str) -> Result<String, String> {
let Some(text) = existing else {
return Ok(format!("{HOOK_TYPES_LINE}\n\nrepos:\n{block}\n"));
};
if let Some(defect) = hooks_marker_defect(text) {
return Err(defect);
}
if let Some(found) = extract_block(text, HOOKS_BEGIN, HOOKS_END) {
return Ok(text.replacen(found, block, 1));
}
let mut out = String::with_capacity(text.len() + block.len() + 1);
let mut placed = false;
for line in text.split_inclusive('\n') {
out.push_str(line);
if !placed && line.trim_end() == "repos:" {
if !out.ends_with('\n') {
out.push('\n');
}
out.push_str(block);
out.push('\n');
placed = true;
}
}
if placed {
Ok(out)
} else {
Err(format!(
"{HOOKS_DESTINATION} exists with no repos: line, so the hook block has nowhere to land"
))
}
}
#[must_use]
pub fn marker_defect(destination: &str, text: &str) -> Option<String> {
let (begin, end) = block_markers(destination)?;
let begins = text.matches(begin).count();
let ends = text.matches(end).count();
if begins > 1 || ends > 1 {
return Some(format!(
"{destination} carries more than one release-kit marker pair; release-kit owns exactly one block"
));
}
match (text.find(begin), text.find(end)) {
(Some(begin), Some(end)) if end > begin => None,
(None, None) => None,
_ => Some(format!(
"{destination} carries an unmatched or misordered release-kit marker, so the block's extent is ambiguous"
)),
}
}
#[must_use]
pub fn hooks_marker_defect(text: &str) -> Option<String> {
marker_defect(HOOKS_DESTINATION, text)
}
fn propose_document(
destination: &str,
existing: Option<&[u8]>,
region: &[u8],
) -> Result<Vec<u8>, String> {
let block = String::from_utf8_lossy(region).into_owned();
if let Some(text) = existing
&& let Some(defect) = marker_defect(destination, &String::from_utf8_lossy(text))
{
return Err(defect);
}
if destination == HOOKS_DESTINATION {
let text = match existing {
None => None,
Some(bytes) => Some(std::str::from_utf8(bytes).map_err(|_| {
format!(
"{destination} is not UTF-8, so the hook block has nowhere to land without rewriting the target's bytes"
)
})?),
};
return splice_hooks_block(text, &block).map(String::into_bytes);
}
Ok(splice_marked_block(existing, &block))
}
#[must_use]
pub fn nix_unsupported_shape(shape: &CrateShape) -> Option<String> {
let Some(text) = shape.cargo_toml.as_deref() else {
return Some(
"the target has no readable Cargo.toml, which the seeded package expression reads; no Nix file lands".to_owned(),
);
};
let Ok(table) = text.parse::<toml::Table>() else {
return Some(
"the target's Cargo.toml does not parse, and the seeded package expression reads it; no Nix file lands".to_owned(),
);
};
if !table.contains_key("package") {
return Some(
"the target's Cargo.toml has no [package] table; the seed supports a single crate, so no Nix file lands".to_owned(),
);
}
if !shape.cargo_lock {
return Some(
"the target has no Cargo.lock, which the seeded package expression builds from; commit one, then opt in".to_owned(),
);
}
let implicit_bin = shape.main_rs
&& table
.get("package")
.and_then(toml::Value::as_table)
.and_then(|package| package.get("autobins"))
.and_then(toml::Value::as_bool)
!= Some(false);
let explicit_bins = table.get("bin").and_then(toml::Value::as_array);
if explicit_bins.is_none() && !implicit_bin {
return Some(
"the target declares no binary — no effective src/main.rs and no [[bin]] entry — and the seed flake's smoke check runs one; no Nix file lands".to_owned(),
);
}
if let Some(bins) = explicit_bins {
let required = bins
.first()
.and_then(toml::Value::as_table)
.and_then(|bin| bin.get("required-features"))
.and_then(toml::Value::as_array);
if let Some(required) = required {
let enabled = default_features(&table);
let missing = required
.iter()
.filter_map(toml::Value::as_str)
.any(|feature| !enabled.contains(feature));
if missing {
return Some(
"the target's first [[bin]] entry requires features a default build does not enable; no Nix file lands".to_owned(),
);
}
}
}
None
}
fn dep_edge_suppresses(features: &toml::Table, name: &str) -> bool {
let edge = format!("dep:{name}");
features.values().any(|list| {
list.as_array().is_some_and(|entries| {
entries
.iter()
.filter_map(toml::Value::as_str)
.any(|entry| entry == edge)
})
})
}
fn is_optional_dependency(table: &toml::Table, name: &str) -> bool {
["dependencies", "build-dependencies"]
.iter()
.any(|section| {
table
.get(*section)
.and_then(toml::Value::as_table)
.and_then(|dependencies| dependencies.get(name))
.and_then(toml::Value::as_table)
.and_then(|dependency| dependency.get("optional"))
.and_then(toml::Value::as_bool)
== Some(true)
})
}
fn default_features(table: &toml::Table) -> std::collections::BTreeSet<String> {
let Some(features) = table.get("features").and_then(toml::Value::as_table) else {
return std::collections::BTreeSet::new();
};
let mut enabled = std::collections::BTreeSet::new();
let mut queue = vec!["default".to_owned()];
while let Some(name) = queue.pop() {
if !enabled.insert(name.clone()) {
continue;
}
if let Some(implies) = features.get(&name).and_then(toml::Value::as_array) {
for implied in implies.iter().filter_map(toml::Value::as_str) {
if implied.starts_with("dep:") || implied.contains("?/") {
continue;
}
if let Some((package, _)) = implied.split_once('/') {
let feature_exists =
features.contains_key(package) || !dep_edge_suppresses(features, package);
if is_optional_dependency(table, package) && feature_exists {
queue.push(package.to_owned());
}
} else {
queue.push(implied.to_owned());
}
}
}
}
enabled
}
#[must_use]
pub fn flake_pair_withheld(
flake_recorded: bool,
flake_nix_present: bool,
flake_lock_present: bool,
) -> Option<String> {
if flake_recorded {
return None;
}
let present: Vec<&str> = [
("flake.nix", flake_nix_present),
("flake.lock", flake_lock_present),
]
.into_iter()
.filter_map(|(name, present)| present.then_some(name))
.collect();
if present.is_empty() {
return None;
}
Some(format!(
"the target already carries {}; its flake pair stays its own",
present.join(" and ")
))
}
#[must_use]
pub fn nix_withholding(
nix: bool,
evidence: &TargetEvidence,
) -> Option<(&'static [&'static str], String)> {
if !nix {
return None;
}
if let Some(reason) = nix_unsupported_shape(&evidence.crate_shape) {
return Some((&NIX_DESTINATIONS[..], reason));
}
flake_pair_withheld(
evidence.flake_recorded,
evidence.flake_nix_present,
evidence.flake_lock_present,
)
.map(|reason| (&NIX_WITHHOLDABLE[..], reason))
}
#[cfg(test)]
mod tests {
use super::{
AGENTS_DESTINATION, BLOCK_BEGIN, BLOCK_DESTINATIONS, BLOCK_END, Candidate, Collision,
CrateShape, GLOSSARY_DESTINATION, HOOK_TYPES_LINE, HOOKS_BEGIN, HOOKS_DESTINATION,
HOOKS_END, Placement, Projection, ProjectionInput, TargetEvidence, extract_block,
select_pair,
};
use crate::landing::{Params, Style};
fn supported_shape() -> CrateShape {
CrateShape {
cargo_toml: Some("[package]\nname = \"widget\"\nversion = \"0.1.0\"\n".to_owned()),
cargo_lock: true,
main_rs: true,
}
}
fn input(evidence: TargetEvidence) -> ProjectionInput {
let mut params = Params::for_test("acme/widget", Some(Style::Trunk));
params.set_nix_for_test(true);
ProjectionInput { params, evidence }
}
fn compute(evidence: TargetEvidence) -> Projection {
Projection::compute(&input(evidence)).expect("the embedded pair projects")
}
fn candidate<'a>(projection: &'a Projection, destination: &str) -> &'a Candidate {
projection
.candidates
.iter()
.find(|candidate| candidate.destination == destination)
.expect("the destination projects")
}
fn outside(bytes: &[u8], begin: &str, end: &str) -> (Vec<u8>, Vec<u8>) {
let text = String::from_utf8_lossy(bytes);
let start = text.find(begin).expect("the begin marker is present");
let stop = text[start..].find(end).expect("the end marker is present") + start + end.len();
(bytes[..start].to_vec(), bytes[stop..].to_vec())
}
#[test]
fn equal_projection_inputs_yield_byte_identical_projections() {
let mut documents = std::collections::BTreeMap::new();
documents.insert(
AGENTS_DESTINATION.to_owned(),
b"# Widget\n\nOwn rules.\n".to_vec(),
);
let evidence = TargetEvidence {
documents,
crate_shape: supported_shape(),
..TargetEvidence::default()
};
let first = input(evidence.clone());
let second = input(evidence);
assert_eq!(first, second, "the inputs are values and compare equal");
let a = Projection::compute(&first).expect("the pair projects");
let b = Projection::compute(&second).expect("the pair projects");
assert_eq!(a.candidates.len(), b.candidates.len());
for (x, y) in a.candidates.iter().zip(&b.candidates) {
assert_eq!(x.destination, y.destination);
assert_eq!(x.kind, y.kind);
assert_eq!(x.placement, y.placement);
assert_eq!(x.bytes, y.bytes, "{}", x.destination);
assert_eq!(x.region, y.region, "{}", x.destination);
assert_eq!(x.sources, y.sources, "{}", x.destination);
}
assert_eq!(a, b);
let destinations: Vec<&str> = a
.candidates
.iter()
.map(|candidate| candidate.destination.as_str())
.collect();
let mut sorted = destinations.clone();
sorted.sort_unstable();
assert_eq!(destinations, sorted, "candidates sort by destination");
assert!(a.omissions.is_empty(), "{:?}", a.omissions);
assert!(a.collisions.is_empty(), "{:?}", a.collisions);
}
#[test]
fn the_projection_performs_no_filesystem_git_environment_clock_registry_or_network_read() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/projection.rs");
let text = std::fs::read_to_string(&path).expect("the source reads");
let production = text.split("#[cfg(test)]").next().unwrap_or("");
let needles = [
"std::fs",
"std::env",
"std::process",
"std::time",
"SystemTime",
"Instant",
"std::net",
"Command::new",
"registry::",
"curl",
"reqwest",
"blob(",
];
let mut hits = Vec::new();
for (index, line) in production.lines().enumerate() {
if line.trim_start().starts_with("//") {
continue;
}
for needle in needles {
if line.contains(needle) {
hits.push(format!("src/projection.rs:{}: {needle}", index + 1));
}
}
}
assert!(
hits.is_empty(),
"the projection reads beyond its inputs: {hits:?}"
);
}
#[test]
#[allow(
clippy::too_many_lines,
reason = "one test walks the three marked destinations and the three unmarked shapes"
)]
fn marked_region_projection_preserves_every_target_byte_outside_the_markers() {
let agents_before = "# Widget\n\nOperator prose above.\n\n";
let agents_after = "\n\n## Our rules\n\nOperator prose below. \n";
let glossary_before = "# Glossary\n\n- `spike` is a throwaway branch.\n\n";
let glossary_after = "\n\n## More terms\n\n- `own` is ours.";
let hooks_before = "default_install_hook_types: [pre-commit]\n\nrepos:\n";
let hooks_after =
"\n - repo: https://example.com/own\n rev: v1\n hooks:\n - id: own\n";
let stale = |begin: &str, end: &str| format!("{begin}\nstale block\n{end}");
let mut documents = std::collections::BTreeMap::new();
documents.insert(
AGENTS_DESTINATION.to_owned(),
format!(
"{agents_before}{}{agents_after}",
stale(BLOCK_BEGIN, BLOCK_END)
)
.into_bytes(),
);
documents.insert(
GLOSSARY_DESTINATION.to_owned(),
format!(
"{glossary_before}{}{glossary_after}",
stale(BLOCK_BEGIN, BLOCK_END)
)
.into_bytes(),
);
documents.insert(
HOOKS_DESTINATION.to_owned(),
format!(
"{hooks_before}{}{hooks_after}",
stale(HOOKS_BEGIN, HOOKS_END)
)
.into_bytes(),
);
let projection = compute(TargetEvidence {
documents: documents.clone(),
crate_shape: supported_shape(),
..TargetEvidence::default()
});
assert!(
projection.collisions.is_empty(),
"{:?}",
projection.collisions
);
for (destination, before, after) in [
(AGENTS_DESTINATION, agents_before, agents_after),
(GLOSSARY_DESTINATION, glossary_before, glossary_after),
(HOOKS_DESTINATION, hooks_before, hooks_after),
] {
let candidate = candidate(&projection, destination);
let Placement::Region { begin, end } = candidate.placement else {
panic!("{destination} is a region");
};
let region = candidate
.region
.as_deref()
.expect("a region carries its block");
let (head, tail) = outside(&candidate.bytes, begin, end);
assert_eq!(
head,
before.as_bytes(),
"{destination}: bytes before the markers"
);
assert_eq!(
tail,
after.as_bytes(),
"{destination}: bytes after the markers"
);
let inside = &candidate.bytes[head.len()..candidate.bytes.len() - tail.len()];
assert_eq!(
inside, region,
"{destination}: the region is the rendered block"
);
let (existing_head, existing_tail) = outside(&documents[destination], begin, end);
assert_eq!(head, existing_head);
assert_eq!(tail, existing_tail);
}
let own_hooks =
"repos:\n - repo: https://example.com/own\n rev: v1\n hooks:\n - id: own\n";
let own_agents = "# Widget\n\nOwn rules.";
let mut documents = std::collections::BTreeMap::new();
documents.insert(HOOKS_DESTINATION.to_owned(), own_hooks.as_bytes().to_vec());
documents.insert(
AGENTS_DESTINATION.to_owned(),
own_agents.as_bytes().to_vec(),
);
let projection = compute(TargetEvidence {
documents,
crate_shape: supported_shape(),
..TargetEvidence::default()
});
assert!(
projection.collisions.is_empty(),
"{:?}",
projection.collisions
);
let hooks = candidate(&projection, HOOKS_DESTINATION);
let hooks_text = String::from_utf8_lossy(&hooks.bytes);
let region = String::from_utf8_lossy(hooks.region.as_deref().expect("a region"));
assert!(
hooks_text.starts_with(&format!(
"repos:\n{region}\n - repo: https://example.com/own"
)),
"{hooks_text}"
);
assert!(!hooks_text.contains(HOOK_TYPES_LINE));
let agents = candidate(&projection, AGENTS_DESTINATION);
assert!(agents.bytes.starts_with(own_agents.as_bytes()));
assert_eq!(
extract_block(
&String::from_utf8_lossy(&agents.bytes),
BLOCK_BEGIN,
BLOCK_END
)
.map(str::as_bytes),
agents.region.as_deref()
);
let glossary = candidate(&projection, GLOSSARY_DESTINATION);
let region = glossary.region.as_deref().expect("a region");
assert_eq!(
glossary.bytes,
[region, b"\n"].concat(),
"an absent file is fresh"
);
}
#[test]
fn a_hook_document_that_is_not_utf8_collides_instead_of_being_rewritten() {
let mut documents = std::collections::BTreeMap::new();
let mut invalid = b"repos:\n# own \xff above\n".to_vec();
invalid.extend_from_slice(format!("{HOOKS_BEGIN}\nstale\n{HOOKS_END}\n").as_bytes());
invalid.extend_from_slice(b" - repo: local \xff below\n");
documents.insert(HOOKS_DESTINATION.to_owned(), invalid);
let mut agents = b"# Widget r\xe9sum\xe9\n\n".to_vec();
agents.extend_from_slice(format!("{BLOCK_BEGIN}\nstale\n{BLOCK_END}\n\n").as_bytes());
agents.extend_from_slice(b"r\xe9sum\xe9\n");
documents.insert(AGENTS_DESTINATION.to_owned(), agents.clone());
let projection = compute(TargetEvidence {
documents,
crate_shape: supported_shape(),
..TargetEvidence::default()
});
let collided: Vec<&str> = projection
.collisions
.iter()
.map(|c| c.destination.as_str())
.collect();
assert_eq!(collided, [HOOKS_DESTINATION]);
assert!(
projection.collisions[0].reason.contains("not UTF-8"),
"{}",
projection.collisions[0].reason
);
assert!(
!projection
.candidates
.iter()
.any(|c| c.destination == HOOKS_DESTINATION),
"a colliding destination projects no candidate"
);
let agents = candidate(&projection, AGENTS_DESTINATION);
assert!(
agents.bytes.starts_with(b"# Widget r\xe9sum\xe9\n\n"),
"{:?}",
agents.bytes
);
assert!(
agents.bytes.ends_with(b"\n\nr\xe9sum\xe9\n"),
"{:?}",
agents.bytes
);
assert!(
!agents.bytes.contains(&0xEF),
"a replacement character landed"
);
let mut documents = std::collections::BTreeMap::new();
let valid =
format!("repos:\n# own above\n{HOOKS_BEGIN}\nstale\n{HOOKS_END}\n - repo: local\n");
documents.insert(HOOKS_DESTINATION.to_owned(), valid.into_bytes());
let projection = compute(TargetEvidence {
documents,
crate_shape: supported_shape(),
..TargetEvidence::default()
});
assert!(
projection.collisions.is_empty(),
"{:?}",
projection.collisions
);
let hooks = candidate(&projection, HOOKS_DESTINATION);
let text = String::from_utf8(hooks.bytes.clone()).expect("a valid document stays text");
assert!(text.starts_with("repos:\n# own above\n"), "{text}");
assert!(text.ends_with("\n - repo: local\n"), "{text}");
assert!(!text.contains("stale"), "the region is replaced: {text}");
}
#[test]
fn a_whole_file_colliding_with_a_marked_region_names_both_source_paths() {
let files: Vec<(String, &[u8])> = vec![
("snippets/_shared/github/SECURITY.md".to_owned(), b"policy"),
("snippets/rust/github/AGENTS.md".to_owned(), b"whole"),
];
let err = Projection::compute_over(&files, &input(TargetEvidence::default()))
.expect_err("a whole file at a block destination refuses");
let text = err.to_string();
assert!(text.contains("snippets/rust/github/AGENTS.md"), "{text}");
assert!(text.contains(super::AGENTS_BLOCK), "{text}");
assert!(text.contains(super::AGENTS_LINE_WORKTREE), "{text}");
assert!(text.contains("embedded sources are defective"), "{text}");
}
#[test]
fn duplicate_whole_file_destinations_and_overlapping_marked_regions_refuse_with_the_conflicting_source_names()
{
let files: Vec<(String, &[u8])> = vec![
("snippets/_shared/github/SECURITY.md".to_owned(), b"shared"),
("snippets/rust/github/SECURITY.md".to_owned(), b"pair"),
("snippets/rust/github/release-plz.toml".to_owned(), b"seed"),
];
let err = select_pair(&files, "rust", "github").expect_err("a doubled destination refuses");
let text = err.to_string();
assert!(
text.contains("snippets/_shared/github/SECURITY.md"),
"{text}"
);
assert!(text.contains("snippets/rust/github/SECURITY.md"), "{text}");
assert!(text.contains("embedded sources are defective"), "{text}");
let clean: Vec<(String, &[u8])> = vec![
("snippets/_shared/github/SECURITY.md".to_owned(), b"shared"),
("snippets/rust/github/release-plz.toml".to_owned(), b"seed"),
];
let selected = select_pair(&clean, "rust", "github").expect("a clean list selects");
let destinations: Vec<&str> = selected.iter().map(|s| s.destination.as_str()).collect();
assert_eq!(destinations, ["SECURITY.md", "release-plz.toml"]);
assert_eq!(selected[0].source, "snippets/_shared/github/SECURITY.md");
let err = select_pair(&clean, "_shared", "github").expect_err("the shared zone is no tech");
assert!(!err.to_string().contains("bindings are: _shared"), "{err}");
let err = select_pair(&clean, "rust", "gitlab").expect_err("an unshipped pair refuses");
assert!(err.to_string().contains("rust, github"), "{err}");
let doubled = format!("{BLOCK_BEGIN}\na\n{BLOCK_END}\n{BLOCK_BEGIN}\nb\n{BLOCK_END}\n");
let unmatched = format!("repos:\n{HOOKS_BEGIN}\n - repo: local\n");
let misordered = format!("# G\n{BLOCK_END}\n{BLOCK_BEGIN}\n");
let mut documents = std::collections::BTreeMap::new();
documents.insert(AGENTS_DESTINATION.to_owned(), doubled.into_bytes());
documents.insert(HOOKS_DESTINATION.to_owned(), unmatched.into_bytes());
documents.insert(GLOSSARY_DESTINATION.to_owned(), misordered.into_bytes());
let projection = compute(TargetEvidence {
documents,
crate_shape: supported_shape(),
..TargetEvidence::default()
});
let mut collided: Vec<&str> = projection
.collisions
.iter()
.map(|Collision { destination, .. }| destination.as_str())
.collect();
collided.sort_unstable();
let mut expected = BLOCK_DESTINATIONS.to_vec();
expected.sort_unstable();
assert_eq!(collided, expected);
for collision in &projection.collisions {
assert!(
collision.reason.contains(&collision.destination),
"{collision:?}"
);
assert!(
!projection
.candidates
.iter()
.any(|candidate| candidate.destination == collision.destination),
"{} collided and still projects",
collision.destination
);
}
let agents = projection
.collisions
.iter()
.find(|c| c.destination == AGENTS_DESTINATION)
.expect("the doubled document collides");
assert!(agents.reason.contains("more than one"), "{}", agents.reason);
let hooks = projection
.collisions
.iter()
.find(|c| c.destination == HOOKS_DESTINATION)
.expect("the unmatched document collides");
assert!(hooks.reason.contains("unmatched"), "{}", hooks.reason);
}
}