simit 0.17.7

Semver-aware git commit helper for Rust projects
Documentation
use std::collections::BTreeMap;

use crate::cargo;
use crate::cli::{
    ChocolateyBumpArgs, ScoopBumpArgs, WindowsAction, WindowsCommand, WindowsPublishArgs,
    WingetSubmitArgs,
};
use crate::config::ProjectConfig;
use anyhow::{Context, Result, bail};

pub fn run(command: WindowsCommand) -> Result<()> {
    match command.action {
        WindowsAction::Publish(args) => publish(args),
    }
}

fn publish(args: WindowsPublishArgs) -> Result<()> {
    validate_version(&args.version)?;
    let metadata = cargo::metadata_for_current_dir()?;
    let workspace_root = metadata.workspace_root.as_std_path();
    let cfg = ProjectConfig::load(workspace_root)?;
    let archives = parse_archives(&args.archive)?;
    let x64 = archives
        .get("x64")
        .context("missing --archive x64=PATH_OR_URL")?;

    let selected = SelectedChannels {
        chocolatey: args.all
            || args.chocolatey
            || no_channel_flags(&args) && cfg.chocolatey.is_some(),
        scoop: args.all || args.scoop || no_channel_flags(&args) && cfg.scoop.is_some(),
        winget: args.all || args.winget || no_channel_flags(&args) && cfg.winget.is_some(),
    };
    if !selected.any() {
        bail!("no Windows channels selected or configured");
    }
    if (selected.chocolatey || selected.scoop) && looks_like_url(x64) {
        bail!("Chocolatey and Scoop require a local x64 archive path for checksum computation");
    }

    if selected.chocolatey {
        let package_dir = args.work_dir.join("chocolatey-package");
        crate::commands::chocolatey::bump(ChocolateyBumpArgs {
            version: args.version.clone(),
            package_dir,
            archive: vec![format!("x64={x64}")],
            push: !args.dry_run,
            push_source: args.choco_push_source.clone(),
            api_key_env: None,
            force_resubmit: args.force_resubmit,
            dry_run: args.dry_run,
            chocolatey: Default::default(),
        })?;
    }
    if selected.scoop {
        crate::commands::scoop::bump(ScoopBumpArgs {
            version: args.version.clone(),
            bucket: None,
            bucket_url: args.scoop_bucket_url.clone(),
            bucket_token_env: args.scoop_bucket_token_env.clone(),
            work_dir: Some(args.work_dir.join("scoop-bucket")),
            archive: vec![format!("x64={x64}")],
            push: !args.dry_run,
            commit_message: None,
            dry_run: args.dry_run,
            scoop: Default::default(),
        })?;
    }
    if selected.winget {
        let url = looks_like_url(x64).then(|| x64.clone());
        crate::commands::winget::submit(WingetSubmitArgs {
            version: args.version,
            package_id: None,
            download_repo: None,
            zip_archive: None,
            url,
            token_env: None,
            wingetcreate: None,
            wingetcreate_url: None,
            work_dir: Some(args.work_dir.join("winget")),
            wine: "wine".to_owned(),
            dry_run: args.dry_run,
        })?;
    }
    Ok(())
}

#[derive(Debug, Clone, Copy)]
struct SelectedChannels {
    chocolatey: bool,
    scoop: bool,
    winget: bool,
}

impl SelectedChannels {
    fn any(self) -> bool {
        self.chocolatey || self.scoop || self.winget
    }
}

fn no_channel_flags(args: &WindowsPublishArgs) -> bool {
    !args.all && !args.chocolatey && !args.scoop && !args.winget
}

fn parse_archives(values: &[String]) -> Result<BTreeMap<String, String>> {
    let mut archives = BTreeMap::new();
    for value in values {
        let (arch, path) = value
            .split_once('=')
            .ok_or_else(|| anyhow::anyhow!("--archive must be ARCH=PATH_OR_URL"))?;
        if arch.is_empty() || path.is_empty() {
            bail!("--archive must be ARCH=PATH_OR_URL");
        }
        if archives.insert(arch.to_owned(), path.to_owned()).is_some() {
            bail!("duplicate archive for {arch}");
        }
    }
    Ok(archives)
}

fn looks_like_url(value: &str) -> bool {
    value.starts_with("https://") || value.starts_with("http://")
}

fn validate_version(version: &str) -> Result<()> {
    if version.starts_with('v')
        || version.is_empty()
        || !version
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '+' | '~' | '_' | '-'))
    {
        bail!("version must match [0-9A-Za-z.+~_-]+ without a leading v, got: {version}");
    }
    Ok(())
}