vership 0.4.3

Multi-target release orchestrator
Documentation
use std::cell::RefCell;
use std::path::{Path, PathBuf};

use super::ProjectType;
use crate::error::{Error, Result};
use crate::version;

pub struct PythonProject {
    modified_files: RefCell<Vec<PathBuf>>,
}

impl PythonProject {
    pub fn new() -> Self {
        Self {
            modified_files: RefCell::new(Vec::new()),
        }
    }

    fn detect_lockfile(root: &Path) -> Option<&'static str> {
        let lockfiles = ["uv.lock", "poetry.lock"];
        lockfiles.into_iter().find(|f| root.join(f).exists())
    }
}

impl Default for PythonProject {
    fn default() -> Self {
        Self::new()
    }
}

impl ProjectType for PythonProject {
    fn name(&self) -> &str {
        "Python"
    }

    fn read_version(&self, root: &Path) -> Result<semver::Version> {
        let path = root.join("pyproject.toml");
        let content = std::fs::read_to_string(&path)
            .map_err(|e| Error::Other(format!("read pyproject.toml: {e}")))?;
        version::parse_pyproject_version(&content)
    }

    fn write_version(&self, root: &Path, new_version: &semver::Version) -> Result<()> {
        let mut modified = self.modified_files.borrow_mut();
        modified.clear();

        let path = root.join("pyproject.toml");
        let content = std::fs::read_to_string(&path)
            .map_err(|e| Error::Other(format!("read pyproject.toml: {e}")))?;

        let updated =
            version::replace_pyproject_version(&content, new_version).ok_or_else(|| {
                Error::Version(
                    "cannot update version in pyproject.toml: version is dynamic or missing"
                        .to_string(),
                )
            })?;

        std::fs::write(&path, updated)
            .map_err(|e| Error::Other(format!("write pyproject.toml: {e}")))?;
        modified.push(PathBuf::from("pyproject.toml"));

        if let Some(lockfile) = Self::detect_lockfile(root) {
            modified.push(PathBuf::from(lockfile));
        }

        Ok(())
    }

    fn verify_lockfile(&self, _root: &Path) -> Result<()> {
        Ok(())
    }

    fn sync_lockfile(&self, root: &Path) -> Result<()> {
        if root.join("uv.lock").exists() {
            let status = std::process::Command::new("uv")
                .args(["lock"])
                .current_dir(root)
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status()
                .map_err(|e| Error::Other(format!("run uv lock: {e}")))?;
            if !status.success() {
                return Err(Error::CheckFailed(
                    "uv lock failed after version bump".to_string(),
                ));
            }
        } else if root.join("poetry.lock").exists() {
            let status = std::process::Command::new("poetry")
                .args(["lock", "--no-update"])
                .current_dir(root)
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .status()
                .map_err(|e| Error::Other(format!("run poetry lock: {e}")))?;
            if !status.success() {
                return Err(Error::CheckFailed(
                    "poetry lock failed after version bump".to_string(),
                ));
            }
        }
        Ok(())
    }

    fn run_lint(&self, _root: &Path) -> Result<()> {
        Ok(())
    }

    fn run_tests(&self, _root: &Path) -> Result<()> {
        Ok(())
    }

    fn modified_files(&self) -> Vec<PathBuf> {
        self.modified_files.borrow().clone()
    }
}