#![warn(
clippy::pedantic,
clippy::unwrap_used,
clippy::clone_on_ref_ptr,
clippy::todo
)]
#![allow(clippy::doc_markdown)]
#![forbid(unsafe_code)]
use std::fs;
use std::path::Path;
use color_eyre::eyre::{Result, WrapErr};
pub mod cli;
mod config;
mod convert;
mod extra_fields;
mod footnote;
mod init;
mod logging;
mod note;
mod references;
mod status_report;
mod summary_list;
mod templating;
mod ticket_abstraction;
mod tracker_access;
use cli::{Cli, Commands};
use templating::{DocumentVariant, Module};
use crate::config::Project;
pub use crate::ticket_abstraction::AbstractTicket;
pub const REGEX_ERROR: &str = "Invalid built-in regular expression.";
pub fn run(cli: &Cli) -> Result<()> {
logging::initialize_logger(cli.verbose)?;
match &cli.command {
Commands::Build { project } => {
build_rn_project(project)?;
}
Commands::Ticket { .. } => {
display_single_ticket()?;
}
Commands::Convert {
legacy_config,
new_config,
} => {
convert::convert(legacy_config, new_config)?;
}
Commands::Init { directory } => init::initialize_directory(directory)
.wrap_err("Failed to initialize the project directory.")?,
}
Ok(())
}
fn display_single_ticket() -> Result<()> {
todo!();
}
fn build_rn_project(project_dir: &Path) -> Result<()> {
let project = Project::new(project_dir)?;
log::info!("Building the release notes project.");
let document = Document::new(&project)?;
document.write_variants(&project.generated_dir)?;
Ok(())
}
struct Document {
internal_modules: Vec<Module>,
external_modules: Vec<Module>,
status_table: String,
json_status: String,
internal_summary: String,
external_summary: String,
}
impl Document {
fn new(project: &Project) -> Result<Self> {
let abstract_tickets =
ticket_abstraction::from_queries(&project.tickets, &project.trackers)?;
let tickets_for_internal = variant_tickets(&abstract_tickets, DocumentVariant::Internal);
let tickets_for_external = variant_tickets(&abstract_tickets, DocumentVariant::External);
let (status_table, json_status) = status_report::analyze_status(&abstract_tickets)?;
let (internal_modules, internal_stats) = templating::format_document(
&tickets_for_internal,
&project.templates,
DocumentVariant::Internal,
project.private_footnote,
);
let (external_modules, external_stats) = templating::format_document(
&tickets_for_external,
&project.templates,
DocumentVariant::External,
project.private_footnote,
);
templating::report_usage_statistics(&internal_stats);
let used_internal: Vec<&AbstractTicket> = tickets_for_internal
.into_iter()
.filter(|t| internal_stats.get(&t.id).copied().unwrap_or(0) > 0)
.collect();
let used_external: Vec<&AbstractTicket> = tickets_for_external
.into_iter()
.filter(|t| external_stats.get(&t.id).copied().unwrap_or(0) > 0)
.collect();
let internal_summary =
summary_list::appendix(&used_internal, DocumentVariant::Internal)?;
let external_summary =
summary_list::appendix(&used_external, DocumentVariant::External)?;
Ok(Self {
internal_modules,
external_modules,
status_table,
json_status,
internal_summary,
external_summary,
})
}
fn write_variant(modules: &[Module], summary: &str, generated_dir: &Path) -> Result<()> {
fs::create_dir_all(generated_dir)?;
Self::write_chapters(modules, generated_dir)?;
let summary_file = generated_dir.join("ref_list-of-tickets-by-component.adoc");
log::debug!("Writing file: {}", summary_file.display());
fs::write(summary_file, summary).wrap_err("Failed to write generated summary appendix.")?;
Ok(())
}
fn write_chapters(modules: &[Module], generated_dir: &Path) -> Result<()> {
for chapter in modules {
let out_file = generated_dir.join(chapter.file_name());
log::debug!("Writing file: {}", out_file.display());
let text = match chapter {
Module::WithContent { text, .. } => text.clone(),
Module::Blank { content_type, title, intro_abstract, module_id, .. } => {
let mut header = format!(":_mod-docs-content-type: {}\n", content_type);
header.push_str(&format!("[id=\"{}\"]\n= {}\n", module_id, title));
if !intro_abstract.is_empty() {
header.push_str(&format!("\n{}\n", intro_abstract));
}
header
}
};
fs::write(out_file, text).wrap_err("Failed to write generated module.")?;
if let Module::WithContent {
included_modules, ..
} = chapter
{
if let Some(included_modules) = included_modules {
Self::write_modules(included_modules, generated_dir)?;
}
}
}
Ok(())
}
fn write_modules(modules: &[Module], generated_dir: &Path) -> Result<()> {
for module in modules {
let (content, sub_modules) = match module {
Module::WithContent { text, included_modules, .. } => (Some(text.clone()), included_modules),
Module::Blank { content_type, title, intro_abstract, module_id, .. } => {
let mut header = format!(":_mod-docs-content-type: {}\n", content_type);
header.push_str(&format!("[id=\"{}\"]\n= {}\n", module_id, title));
if !intro_abstract.is_empty() {
header.push_str(&format!("\n{}\n", intro_abstract));
}
(Some(header), &None) }
};
if let Some(text) = content {
let out_file = generated_dir.join(module.file_name());
log::debug!("Writing file: {}", out_file.display());
fs::write(out_file, text).wrap_err("Failed to write generated module.")?;
}
if let Some(included_modules) = sub_modules {
Self::write_modules(included_modules, generated_dir)?;
}
}
Ok(())
}
fn write_variants(&self, generated_dir: &Path) -> Result<()> {
log::info!("Saving the generated release notes.");
if generated_dir.exists() {
fs::remove_dir_all(generated_dir)?;
}
let internal_dir = generated_dir.join("internal");
let external_dir = generated_dir.join("external");
Self::write_variant(
&self.internal_modules,
&self.internal_summary,
&internal_dir,
)?;
Self::write_variant(
&self.external_modules,
&self.external_summary,
&external_dir,
)?;
let html_status_file = generated_dir.join("status-table.html");
log::debug!("Writing file: {}", html_status_file.display());
fs::write(html_status_file, &self.status_table)
.wrap_err("Failed to write the status table.")?;
let json_status_file = generated_dir.join("status-table.json");
log::debug!("Writing file: {}", json_status_file.display());
fs::write(json_status_file, &self.json_status)
.wrap_err("Failed to write the JSON status.")?;
Ok(())
}
}
fn variant_tickets(
all_tickets: &[AbstractTicket],
variant: DocumentVariant,
) -> Vec<&AbstractTicket> {
match variant {
DocumentVariant::Internal => all_tickets.iter().collect(),
DocumentVariant::External => all_tickets
.iter()
.filter(|t| t.doc_text_status == extra_fields::DocTextStatus::Approved)
.collect(),
}
}