Skip to main content

rust_bucket/
upgrade.rs

1// Upgrade command implementation — regenerates managed files and collects migrations
2
3use crate::config::{Config, ConfigError};
4use crate::generator::{self, GeneratorError};
5use crate::migrations::{self, Migration, MigrationError};
6use crate::templates::{self, TemplateError};
7use crate::verify::{self, VerifyError, VerifyReport};
8use semver::Version;
9use std::path::{Path, PathBuf};
10use thiserror::Error;
11
12/// Result of running the upgrade command
13#[derive(Debug)]
14pub struct UpgradeResult {
15    pub old_version: String,
16    pub new_version: String,
17    pub files_generated: Vec<PathBuf>,
18    pub migrations: Vec<Migration>,
19    pub verification: VerifyReport,
20}
21
22/// Errors that can occur during the upgrade operation
23#[derive(Debug, Error)]
24pub enum UpgradeError {
25    /// Target directory is not a Rust crate (no Cargo.toml found)
26    #[error("Not a Rust crate: Cargo.toml not found in target directory")]
27    NotRustCrate,
28
29    /// Target directory is not a git repository (no .git/ found)
30    #[error("Not a git repository: .git/ directory not found")]
31    NotGitRepo,
32
33    /// Target directory is not initialized by rust-bucket (no rust-bucket.toml)
34    #[error(
35        "Not initialized: rust-bucket.toml not found. Use 'rust-bucket apply' to initialize first."
36    )]
37    NotInitialized,
38
39    /// Configuration-related error
40    #[error("Configuration error: {0}")]
41    ConfigError(#[from] ConfigError),
42
43    /// Template generation error
44    #[error("Generator error: {0}")]
45    GeneratorError(#[from] GeneratorError),
46
47    /// Verification error
48    #[error("Verification error: {0}")]
49    VerifyError(#[from] VerifyError),
50
51    /// Template extraction error
52    #[error("Template error: {0}")]
53    TemplateError(#[from] TemplateError),
54
55    /// Migration error
56    #[error("Migration error: {0}")]
57    MigrationError(#[from] MigrationError),
58
59    /// Version parsing error
60    #[error("Invalid version '{0}': {1}")]
61    VersionParse(String, semver::Error),
62}
63
64/// Run the upgrade command on a target directory.
65///
66/// Implements the upgrade flow described in ARCHITECTURE.md.
67pub fn run_upgrade(target_dir: &Path) -> Result<UpgradeResult, UpgradeError> {
68    if !target_dir.join("Cargo.toml").exists() {
69        return Err(UpgradeError::NotRustCrate);
70    }
71
72    if !target_dir.join(".git").exists() {
73        return Err(UpgradeError::NotGitRepo);
74    }
75
76    if !generator::has_rust_bucket_toml(target_dir) {
77        return Err(UpgradeError::NotInitialized);
78    }
79
80    let config_path = target_dir.join("rust-bucket.toml");
81    let mut config = Config::load(&config_path)?;
82    let old_version_str = config.rust_bucket_version.clone();
83    let new_version_str = env!("CARGO_PKG_VERSION").to_string();
84
85    let old_version = Version::parse(&old_version_str)
86        .map_err(|e| UpgradeError::VersionParse(old_version_str.clone(), e))?;
87    let new_version = Version::parse(&new_version_str)
88        .map_err(|e| UpgradeError::VersionParse(new_version_str.clone(), e))?;
89
90    let migrations_list = migrations::migrations_between(&old_version, &new_version)?;
91
92    config.rust_bucket_version = new_version_str.clone();
93    config.save(&config_path)?;
94
95    let (_temp_dir, temp_path) = templates::extract_to_temp()?;
96    let mut files_generated = generator::render(&temp_path, target_dir, &config, true)?;
97
98    let claude_symlink = generator::create_claude_symlink(target_dir)?;
99    files_generated.push(claude_symlink);
100
101    let verification = verify::run_all(target_dir)?;
102
103    Ok(UpgradeResult {
104        old_version: old_version_str,
105        new_version: new_version_str,
106        files_generated,
107        migrations: migrations_list,
108        verification,
109    })
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use tempfile::TempDir;
116
117    #[test]
118    fn test_upgrade_not_rust_crate() -> Result<(), Box<dyn std::error::Error>> {
119        let temp_dir = TempDir::new()?;
120        let result = run_upgrade(temp_dir.path());
121        assert!(matches!(result.unwrap_err(), UpgradeError::NotRustCrate));
122        Ok(())
123    }
124
125    #[test]
126    fn test_upgrade_not_git_repo() -> Result<(), Box<dyn std::error::Error>> {
127        let temp_dir = TempDir::new()?;
128        std::fs::write(
129            temp_dir.path().join("Cargo.toml"),
130            "[package]\nname = \"test\"",
131        )?;
132        let result = run_upgrade(temp_dir.path());
133        assert!(matches!(result.unwrap_err(), UpgradeError::NotGitRepo));
134        Ok(())
135    }
136
137    #[test]
138    fn test_upgrade_not_initialized() -> Result<(), Box<dyn std::error::Error>> {
139        let temp_dir = TempDir::new()?;
140        std::fs::write(
141            temp_dir.path().join("Cargo.toml"),
142            "[package]\nname = \"test\"",
143        )?;
144        std::fs::create_dir(temp_dir.path().join(".git"))?;
145        let result = run_upgrade(temp_dir.path());
146        assert!(matches!(result.unwrap_err(), UpgradeError::NotInitialized));
147        Ok(())
148    }
149}