use super::identity::records_config_file;
use super::StackStamp;
use crate::core::types::GlobalLock;
use std::path::Path;
pub(super) fn retire_renamed(
lock: &mut GlobalLock,
state_dir: &Path,
config: (&str, Option<&Path>),
) -> Vec<StackStamp> {
let (config_name, config_file) = config;
let renamed: Vec<String> = lock
.stacks
.iter()
.filter(|(stack, stamp)| {
stack.as_str() != config_name && records_config_file(stamp, state_dir, config_file)
})
.map(|(stack, _)| stack.clone())
.collect();
let mut retired = Vec::new();
for old in renamed {
let Some(stamp) = lock.stacks.shift_remove(&old) else {
continue;
};
eprintln!(
"note: stack '{old}' renamed to '{config_name}' in {}",
state_dir.display()
);
retired.push(stamp);
}
retired
}
fn union(first: Vec<String>, second: &[String]) -> Vec<String> {
let mut merged = first;
for item in second {
if !merged.contains(item) {
merged.push(item.clone());
}
}
merged
}
pub(super) fn next_stamp(
previous: Option<&StackStamp>,
retired: &[StackStamp],
stamped_file: Option<String>,
written: Vec<String>,
declared: Option<&[String]>,
when: (&str, &str), ) -> StackStamp {
let (last_apply, generator) = when;
StackStamp {
file: stamped_file
.or_else(|| previous.and_then(|s| s.file.clone()))
.or_else(|| retired.iter().find_map(|s| s.file.clone())),
machines: claimed_machines(previous, retired, written, declared),
outputs: inherited_outputs(previous, retired),
last_apply: last_apply.to_string(),
generator: generator.to_string(),
}
}
fn claimed_machines(
previous: Option<&StackStamp>,
retired: &[StackStamp],
written: Vec<String>,
declared: Option<&[String]>,
) -> Vec<String> {
let mut machines = written;
if declared.is_some() {
machines = union(machines, previous.map_or(&[], |s| s.machines.as_slice()));
}
for stamp in retired {
machines = union(machines, &stamp.machines);
}
match declared {
Some(declared) => {
machines.retain(|m| declared.contains(m));
machines
}
None => machines,
}
}
fn inherited_outputs(previous: Option<&StackStamp>, retired: &[StackStamp]) -> Vec<String> {
let mut outputs = previous.map(|s| s.outputs.clone()).unwrap_or_default();
for stamp in retired {
outputs = union(outputs, &stamp.outputs);
}
outputs
}
#[cfg(test)]
mod tests {
use super::*;
fn names(of: &[&str]) -> Vec<String> {
of.iter().map(|s| (*s).to_string()).collect()
}
fn stamp_owning(machines: &[&str]) -> StackStamp {
StackStamp {
file: None,
last_apply: "2026-09-06T00:00:00Z".to_string(),
generator: "forjar test".to_string(),
machines: names(machines),
outputs: Vec::new(),
}
}
fn machines_of(previous: &[&str], written: &[&str], declared: Option<&[&str]>) -> Vec<String> {
let previous = stamp_owning(previous);
let declared = declared.map(names);
claimed_machines(Some(&previous), &[], names(written), declared.as_deref())
}
#[test]
fn a_declared_machine_this_apply_skipped_is_still_owned() {
assert_eq!(
machines_of(
&["mini", "lambda-labs"],
&["mini"],
Some(&["mini", "lambda-labs"])
),
names(&["mini", "lambda-labs"]),
);
}
#[test]
fn a_machine_the_config_stopped_declaring_is_released() {
assert_eq!(
machines_of(&["mini", "lambda-labs"], &["mini"], Some(&["mini"])),
names(&["mini"]),
);
}
#[test]
fn without_a_declaration_the_stamp_is_what_the_apply_wrote() {
assert_eq!(
machines_of(&["mini", "lambda-labs"], &["mini"], None),
names(&["mini"])
);
}
#[test]
fn a_retired_predecessors_machines_move_and_are_still_bounded() {
let retired = [stamp_owning(&["lambda-labs", "gone"])];
assert_eq!(
claimed_machines(
None,
&retired,
names(&["mini"]),
Some(&names(&["mini", "lambda-labs"]))
),
names(&["mini", "lambda-labs"]),
);
}
}