fledge 1.1.1

Dev lifecycle CLI. One tool for the dev loop, any language.
use anyhow::{bail, Context, Result};
use console::style;
use std::path::Path;
use std::process::Command;

use crate::versioning::Version;

pub(super) fn create_release_commit(
    dir: &Path,
    version: &Version,
    bumped_files: &[String],
    has_changelog: bool,
    quiet: bool,
) -> Result<()> {
    let mut files_to_add: Vec<String> = bumped_files.to_vec();
    if has_changelog && dir.join("CHANGELOG.md").exists() {
        files_to_add.push("CHANGELOG.md".to_string());
    }

    // Filter out gitignored files to avoid staging failures (e.g. Cargo.lock)
    let ignored = Command::new("git")
        .arg("check-ignore")
        .args(&files_to_add)
        .current_dir(dir)
        .output()
        .ok()
        .map(|o| {
            String::from_utf8_lossy(&o.stdout)
                .lines()
                .map(String::from)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    files_to_add.retain(|f| !ignored.contains(f));

    if !files_to_add.is_empty() {
        let mut cmd = Command::new("git");
        cmd.arg("add").current_dir(dir);
        for f in &files_to_add {
            cmd.arg(f);
        }
        let output = cmd.output().context("staging release files")?;
        if !output.status.success() {
            bail!(
                "git add failed: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }
    }

    let msg = format!("chore: release v{version}");
    let mut commit_args = vec!["commit", "-m", &msg];
    if files_to_add.is_empty() {
        commit_args.push("--allow-empty");
    }
    let output = Command::new("git")
        .args(&commit_args)
        .current_dir(dir)
        .output()
        .context("creating release commit")?;

    if !output.status.success() {
        bail!(
            "git commit failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    if !quiet {
        println!("  Created commit: {}", style(&msg).dim());
    }
    Ok(())
}

pub(super) fn create_tag(dir: &Path, version: &Version, quiet: bool) -> Result<()> {
    let tag = format!("v{version}");

    let check = Command::new("git")
        .args(["tag", "-l", &tag])
        .current_dir(dir)
        .output()
        .context("checking existing tags")?;
    if !String::from_utf8_lossy(&check.stdout).trim().is_empty() {
        bail!(
            "Tag '{}' already exists. Delete it first with: git tag -d {}",
            tag,
            tag
        );
    }

    let output = Command::new("git")
        .args(["tag", "-a", &tag, "-m", &format!("Release {tag}")])
        .current_dir(dir)
        .output()
        .context("creating git tag")?;

    if !output.status.success() {
        bail!(
            "git tag failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    if !quiet {
        println!("  Created tag: {}", style(&tag).cyan());
    }
    Ok(())
}

pub(super) fn push_release(
    dir: &Path,
    version: &Version,
    has_tag: bool,
    quiet: bool,
) -> Result<()> {
    let output = Command::new("git")
        .args(["push"])
        .current_dir(dir)
        .output()
        .context("pushing release commit")?;

    if !output.status.success() {
        bail!(
            "git push failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }

    if has_tag {
        let tag = format!("v{version}");
        let output = Command::new("git")
            .args(["push", "origin", &tag])
            .current_dir(dir)
            .output()
            .context("pushing release tag")?;

        if !output.status.success() {
            bail!(
                "git push tag failed: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }
    }

    if !quiet {
        println!("  Pushed to remote");
    }
    Ok(())
}