feature-manifest 0.7.6

Document, validate, and render Cargo feature metadata.
Documentation
use std::fs;

use anyhow::{Context, Result, bail};

use crate::cli::util::pluralized;
use crate::{
    SyncOptions, SyncReport, WorkspaceManifest, preview_sync_manifest, render_sync_diff,
    sync_manifest,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncCommandOptions {
    pub sync: SyncOptions,
    pub diff: bool,
}

pub fn run(workspace: &WorkspaceManifest, options: SyncCommandOptions) -> Result<()> {
    let mut changed_packages = 0usize;
    let mut check_failed = false;

    for package in &workspace.packages {
        let preview = if options.diff {
            Some(preview_sync_manifest(
                &package.manifest_path,
                &options.sync,
            )?)
        } else {
            None
        };
        let report = match &preview {
            Some(preview) => preview.report.clone(),
            None => sync_manifest(&package.manifest_path, &options.sync)?,
        };
        let package_name = report.package_name.as_deref().unwrap_or("unknown-package");

        if report.changed() {
            changed_packages += 1;
            if options.sync.check_only || options.diff {
                check_failed = true;
                println!(
                    "sync drift in `{package_name}`: {}",
                    change_summary(&report)
                );
            } else {
                println!("synced `{package_name}`: {}", change_summary(&report));
            }

            for feature in &report.added_features {
                println!("  + {feature}");
            }
            for feature in &report.removed_features {
                println!("  - {feature}");
            }

            if let Some(preview) = &preview {
                if let Some(rewritten) = &preview.rewritten {
                    let before = fs::read_to_string(&package.manifest_path).with_context(|| {
                        format!(
                            "failed to read manifest `{}` for diff output",
                            package.manifest_path.display()
                        )
                    })?;
                    print!(
                        "{}",
                        render_sync_diff(&package.manifest_path, &before, rewritten)
                    );
                }
            }
        } else {
            println!("`{package_name}` is already in sync");
        }
    }

    if changed_packages > 0 && !options.sync.check_only && !options.diff {
        println!("updated {changed_packages} package(s)");
    }

    if check_failed {
        bail!("sync drift detected");
    }

    Ok(())
}

pub fn change_summary(report: &SyncReport) -> String {
    let mut parts = Vec::new();
    if !report.added_features.is_empty() {
        parts.push(format!(
            "added {}",
            pluralized(report.added_features.len(), "feature")
        ));
    }
    if !report.removed_features.is_empty() {
        parts.push(format!(
            "removed {} stale metadata entr{}",
            report.removed_features.len(),
            if report.removed_features.len() == 1 {
                "y"
            } else {
                "ies"
            }
        ));
    }
    if parts.is_empty() {
        parts.push("layout updated".to_owned());
    }
    parts.push(format!("using `{}` layout", report.style));
    parts.join(", ")
}