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/// This function:
67/// 1. Asserts Cargo.toml and .git/ exist
68/// 2. Asserts rust-bucket.toml exists (else error: use `rust-bucket apply` first)
69/// 3. Loads config, captures old_version
70/// 4. Parses old/new versions with semver
71/// 5. Collects migrations between old and new
72/// 6. Updates config version, saves
73/// 7. Extracts templates, renders with overwrite=true
74/// 8. Creates CLAUDE.md symlink
75/// 9. Runs verification
76/// 10. Returns UpgradeResult with migrations
77pub fn run_upgrade(target_dir: &Path) -> Result<UpgradeResult, UpgradeError> {
78    // Step 1: Assert Cargo.toml exists
79    if !target_dir.join("Cargo.toml").exists() {
80        return Err(UpgradeError::NotRustCrate);
81    }
82
83    // Step 2: Assert .git/ exists
84    if !target_dir.join(".git").exists() {
85        return Err(UpgradeError::NotGitRepo);
86    }
87
88    // Step 3: Assert rust-bucket.toml exists
89    if !generator::has_rust_bucket_toml(target_dir) {
90        return Err(UpgradeError::NotInitialized);
91    }
92
93    // Step 4: Load config, capture old version
94    let config_path = target_dir.join("rust-bucket.toml");
95    let mut config = Config::load(&config_path)?;
96    let old_version_str = config.rust_bucket_version.clone();
97    let new_version_str = env!("CARGO_PKG_VERSION").to_string();
98
99    // Step 5: Parse versions
100    let old_version = Version::parse(&old_version_str)
101        .map_err(|e| UpgradeError::VersionParse(old_version_str.clone(), e))?;
102    let new_version = Version::parse(&new_version_str)
103        .map_err(|e| UpgradeError::VersionParse(new_version_str.clone(), e))?;
104
105    // Step 6: Collect migrations
106    let migrations_list = migrations::migrations_between(&old_version, &new_version)?;
107
108    // Step 7: Update config version, save
109    config.rust_bucket_version = new_version_str.clone();
110    config.save(&config_path)?;
111
112    // Step 8: Extract templates, render with overwrite=true
113    let (_temp_dir, temp_path) = templates::extract_to_temp()?;
114    let mut files_generated = generator::render(&temp_path, target_dir, &config, true)?;
115
116    // Step 9: Create CLAUDE.md symlink
117    let claude_symlink = generator::create_claude_symlink(target_dir)?;
118    files_generated.push(claude_symlink);
119
120    // Step 10: Run verification
121    let verification = verify::run_all(target_dir)?;
122
123    Ok(UpgradeResult {
124        old_version: old_version_str,
125        new_version: new_version_str,
126        files_generated,
127        migrations: migrations_list,
128        verification,
129    })
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use tempfile::TempDir;
136
137    #[test]
138    fn test_upgrade_not_rust_crate() {
139        let temp_dir = TempDir::new().unwrap();
140        let result = run_upgrade(temp_dir.path());
141        assert!(matches!(result.unwrap_err(), UpgradeError::NotRustCrate));
142    }
143
144    #[test]
145    fn test_upgrade_not_git_repo() {
146        let temp_dir = TempDir::new().unwrap();
147        std::fs::write(
148            temp_dir.path().join("Cargo.toml"),
149            "[package]\nname = \"test\"",
150        )
151        .unwrap();
152        let result = run_upgrade(temp_dir.path());
153        assert!(matches!(result.unwrap_err(), UpgradeError::NotGitRepo));
154    }
155
156    #[test]
157    fn test_upgrade_not_initialized() {
158        let temp_dir = TempDir::new().unwrap();
159        std::fs::write(
160            temp_dir.path().join("Cargo.toml"),
161            "[package]\nname = \"test\"",
162        )
163        .unwrap();
164        std::fs::create_dir(temp_dir.path().join(".git")).unwrap();
165        let result = run_upgrade(temp_dir.path());
166        assert!(matches!(result.unwrap_err(), UpgradeError::NotInitialized));
167    }
168}