mod registry;
use crate::OutputFormat;
use crate::cmd::output::{print_json_array, table_with_bold_headers};
use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult, Diagnostics};
use crate::load::load_rfcs;
use crate::parse::{load_adrs, load_guards_with_warnings, load_work_items};
use comfy_table::Cell;
use registry::{
get_allowed_tags, has_allowed_tag, read_config_table, set_allowed_tags, validate_tag_format,
write_config_table,
};
pub(crate) use registry::{validate_artifact_tag, validate_registered_tag};
use serde::Serialize;
use std::collections::HashMap;
#[derive(Serialize)]
struct TagEntry {
tag: String,
usage: usize,
}
pub fn tag_new(
config: &Config,
tag: &str,
op: crate::write::WriteOp,
) -> DiagnosticResult<Diagnostics> {
validate_tag_format(tag)?;
let mut table = read_config_table(config)?;
let mut allowed = get_allowed_tags(&table)?;
if has_allowed_tag(&allowed, tag) {
return Err(Diagnostic::new(
DiagnosticCode::E1102TagAlreadyExists,
format!("Tag '{tag}' already exists in [tags] allowed"),
tag,
));
}
allowed.push(tag.to_string());
set_allowed_tags(&mut table, allowed)?;
if !op.is_preview() {
write_config_table(config, &table)?;
println!("Added tag: {tag}");
} else {
println!("Would add tag: {tag}");
}
Ok(vec![])
}
pub fn tag_delete(
config: &Config,
tag: &str,
op: crate::write::WriteOp,
) -> DiagnosticResult<Diagnostics> {
let mut table = read_config_table(config)?;
let allowed = get_allowed_tags(&table)?;
if !has_allowed_tag(&allowed, tag) {
return Err(Diagnostic::new(
DiagnosticCode::E1103TagNotFound,
format!("Tag '{tag}' not found in [tags] allowed"),
tag,
));
}
let usage_map = build_tag_usage_map(config)?;
let usage = usage_map.get(tag).copied().unwrap_or(0);
if usage > 0 {
return Err(Diagnostic::new(
DiagnosticCode::E1104TagStillReferenced,
format!(
"Cannot delete tag '{tag}': still used by {usage} artifact(s). Remove the tag from all artifacts first."
),
tag,
));
}
let new_allowed: Vec<String> = allowed.into_iter().filter(|t| t != tag).collect();
set_allowed_tags(&mut table, new_allowed)?;
if !op.is_preview() {
write_config_table(config, &table)?;
println!("Deleted tag: {tag}");
} else {
println!("Would delete tag: {tag}");
}
Ok(vec![])
}
pub fn tag_list(config: &Config, output: OutputFormat) -> DiagnosticResult<Diagnostics> {
let table = read_config_table(config)?;
let allowed = get_allowed_tags(&table)?;
let usage_map = build_tag_usage_map(config)?;
let entries: Vec<TagEntry> = allowed
.iter()
.map(|tag| {
let usage = usage_map.get(tag).copied().unwrap_or(0);
TagEntry {
tag: tag.clone(),
usage,
}
})
.collect();
print_tag_entries(&entries, output);
Ok(vec![])
}
fn print_tag_entries(entries: &[TagEntry], output: OutputFormat) {
match output {
OutputFormat::Json => {
print_json_array(entries);
}
OutputFormat::Plain => {
for entry in entries {
println!("{}\t{}", entry.tag, entry.usage);
}
}
OutputFormat::Table => {
let mut table = table_with_bold_headers(&["Tag", "Usage"]);
for entry in entries {
table.add_row(vec![
Cell::new(&entry.tag),
Cell::new(entry.usage.to_string()),
]);
}
println!("{table}");
}
}
}
fn build_tag_usage_map(config: &Config) -> DiagnosticResult<HashMap<String, usize>> {
let mut usage: HashMap<String, usize> = HashMap::new();
let rfcs = load_rfcs(config).map_err(Diagnostic::from)?;
for rfc_index in &rfcs {
increment_tag_usage(&mut usage, &rfc_index.rfc.tags);
for clause in &rfc_index.clauses {
increment_tag_usage(&mut usage, &clause.spec.tags);
}
}
let adrs = load_adrs(config)?;
for adr in &adrs {
increment_tag_usage(&mut usage, &adr.spec.govctl.tags);
}
let items = load_work_items(config)?;
for item in &items {
increment_tag_usage(&mut usage, &item.spec.govctl.tags);
}
let guard_result = load_guards_with_warnings(config)?;
for guard in &guard_result.items {
increment_tag_usage(&mut usage, &guard.spec.govctl.tags);
}
Ok(usage)
}
fn increment_tag_usage(map: &mut HashMap<String, usize>, tags: &[String]) {
for tag in tags {
*map.entry(tag.clone()).or_insert(0) += 1;
}
}