wsb 0.124.3689

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
use anyhow::{Context, Result};
use std::path::Path;

use crate::state::{
    calculate_workspace_version, update_version_file_in_repo, StateConfig, TemplateManager,
};
use crate::workspace_state::WorkspaceState;

#[derive(Debug, Clone)]
pub struct WorkspaceUpdateResult {
    pub version: String,
    pub rendered_tera_files: Vec<String>,
    pub rendered_wstemplate_files: Vec<String>,
}

pub fn run_workspace_update(project_root: &Path) -> Result<WorkspaceUpdateResult> {
    let config = StateConfig::load(project_root)?;
    let workspace_state = WorkspaceState::load(project_root)?;
    let version_info = calculate_workspace_version(project_root)?;

    update_version_file_in_repo(&version_info, &config, project_root)?;

    let template_manager = TemplateManager::new(&workspace_state)?;
    let project_name = workspace_state.project_name.as_deref();
    let rendered_tera_files = template_manager.render_all_templates(&version_info, project_name)?;

    let entry = workspace_state.effective_wstemplate_entry();
    let engine = crate::state::wstemplate::WstemplateEngine::new(
        version_info.clone(),
        workspace_state.project_name.clone(),
        entry.alias,
        project_root.to_path_buf(),
        entry.root,
    );

    let rendered = engine.render_relevant()?;
    let rendered_wstemplate_files = rendered
        .into_iter()
        .map(|item| {
            item.output_path
                .strip_prefix(project_root)
                .unwrap_or(item.output_path.as_path())
                .display()
                .to_string()
        })
        .collect::<Vec<_>>();

    workspace_state
        .save(project_root)
        .with_context(|| format!("failed to save workspace state in {}", project_root.display()))?;

    Ok(WorkspaceUpdateResult {
        version: version_info.full_version,
        rendered_tera_files,
        rendered_wstemplate_files,
    })
}

pub async fn run_workspace_update_async(project_root: &Path) -> Result<WorkspaceUpdateResult> {
    let config = StateConfig::load_async(project_root).await?;
    let workspace_state = WorkspaceState::load(project_root)?;
    let version_info = crate::state::calculate_workspace_version_async(project_root).await?;

    update_version_file_in_repo(&version_info, &config, project_root)?;

    let template_manager = TemplateManager::new(&workspace_state)?;
    let project_name = workspace_state.project_name.as_deref();
    let rendered_tera_files = template_manager.render_all_templates(&version_info, project_name)?;

    let entry = workspace_state.effective_wstemplate_entry();
    let engine = crate::state::wstemplate::WstemplateEngine::new(
        version_info.clone(),
        workspace_state.project_name.clone(),
        entry.alias,
        project_root.to_path_buf(),
        entry.root,
    );

    let rendered = engine.render_relevant()?;
    let rendered_wstemplate_files = rendered
        .into_iter()
        .map(|item| {
            item.output_path
                .strip_prefix(project_root)
                .unwrap_or(item.output_path.as_path())
                .display()
                .to_string()
        })
        .collect::<Vec<_>>();

    workspace_state
        .save(project_root)
        .with_context(|| format!("failed to save workspace state in {}", project_root.display()))?;

    Ok(WorkspaceUpdateResult {
        version: version_info.full_version,
        rendered_tera_files,
        rendered_wstemplate_files,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    fn run_git(repo_root: &Path, args: &[&str]) {
        let repo = match args {
            ["init"] => crate::git_backend::init_repository(repo_root).unwrap(),
            _ => crate::git_backend::open_repo(repo_root).unwrap(),
        };

        match args {
            ["init"] => {}
            ["config", "user.name", name] => {
                let email = repo
                    .config_snapshot()
                    .string("user.email")
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "test@example.com".to_string());
                crate::git_backend::configure_local_user(&repo, name, &email).unwrap();
            }
            ["config", "user.email", email] => {
                let name = repo
                    .config_snapshot()
                    .string("user.name")
                    .map(|value| value.to_string())
                    .unwrap_or_else(|| "Test User".to_string());
                crate::git_backend::configure_local_user(&repo, &name, email).unwrap();
            }
            ["add", path] => {
                crate::git_backend::stage_paths(&repo, &[path.to_string()]).unwrap();
            }
            ["commit", "-m", message] => {
                crate::git_backend::commit_all(&repo, message).unwrap();
            }
            _ => panic!("unsupported git test helper command: {args:?}"),
        }
    }

    #[test]
    fn run_workspace_update_writes_into_selected_repo_root() {
        let temp_dir = TempDir::new().unwrap();
        let project_root = temp_dir.path();

        run_git(project_root, &["init"]);
        run_git(project_root, &["config", "user.name", "Test User"]);
        run_git(project_root, &["config", "user.email", "test@example.com"]);

        fs::write(project_root.join("README.md"), "hello\n").unwrap();
        run_git(project_root, &["add", "README.md"]);
        run_git(project_root, &["commit", "-m", "initial"]);

        let _ = StateConfig::load(project_root).unwrap();
        let mut workspace_state = WorkspaceState::load(project_root).unwrap();
        workspace_state.project_name = Some("Example".to_string());
        workspace_state.save(project_root).unwrap();

        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let db_path = project_root.join(".wsb/project.db");
            let database_url = format!("sqlite:{}", db_path.display());
            let pool = sqlx::SqlitePool::connect(&database_url).await.unwrap();
            sqlx::query(
                "UPDATE projects SET major_version = 5 WHERE id = (SELECT id FROM projects LIMIT 1)",
            )
            .execute(&pool)
            .await
            .unwrap();
        });

        let templates_dir = project_root.join(".wsb/templates");
        fs::create_dir_all(&templates_dir).unwrap();
        fs::write(templates_dir.join("version.tera"), "{{ project.version }}\n").unwrap();
        fs::write(
            templates_dir.join("templates.json"),
            r#"{"version":{"name":"version","source_path":"version.tera","output_path":"generated/version.out","enabled":true,"description":null}}"#,
        )
        .unwrap();

        let original_dir = std::env::current_dir().unwrap();
        let unrelated_dir = TempDir::new().unwrap();
        std::env::set_current_dir(unrelated_dir.path()).unwrap();
        let result = run_workspace_update(project_root).unwrap();
        std::env::set_current_dir(original_dir).unwrap();

        assert_eq!(result.version, "5.1.1");
        assert_eq!(
            fs::read_to_string(project_root.join("version.txt")).unwrap().trim(),
            "5.1.1"
        );
        assert_eq!(
            fs::read_to_string(project_root.join("generated/version.out")).unwrap().trim(),
            "5.1.1"
        );
    }
}