Skip to main content

zoi_transaction/
rollback.rs

1use anyhow::{Result, anyhow};
2use colored::*;
3use semver::Version;
4use std::fs;
5use std::path::PathBuf;
6use zoi_core::{sysroot, types, utils as core_utils};
7use zoi_resolver::{local, resolve};
8
9pub fn run(package_name: &str, yes: bool) -> Result<()> {
10    println!("Attempting to roll back '{}'...", package_name.cyan());
11
12    let request = resolve::parse_source_string(package_name)?;
13    let sub_package = request.sub_package.clone();
14    let scope_order = [
15        types::Scope::User,
16        types::Scope::System,
17        types::Scope::Project,
18    ];
19    let mut current_manifest = None;
20    let mut scope = None;
21    for candidate_scope in scope_order {
22        let mut matches = local::find_installed_manifests_matching(&request, candidate_scope)?;
23        match matches.len() {
24            0 => continue,
25            1 => {
26                current_manifest = Some(matches.remove(0));
27                scope = Some(candidate_scope);
28                break;
29            }
30            _ => {
31                return Err(anyhow!(
32                    "Package '{}' is ambiguous in {:?} scope. Use an explicit source like '#handle@repo/name[:sub]@version'.",
33                    request.name,
34                    candidate_scope
35                ));
36            }
37        }
38    }
39
40    let Some(current_manifest) = current_manifest else {
41        return Err(anyhow!("Package '{}' is not installed.", package_name));
42    };
43    let scope = scope.ok_or_else(|| anyhow!("scope should be set when a manifest is found"))?;
44
45    let package_dir = local::get_package_dir(
46        scope,
47        &current_manifest.registry_handle,
48        &current_manifest.repo,
49        &current_manifest.name,
50    )?;
51
52    let mut versions = Vec::new();
53    if let Ok(entries) = fs::read_dir(&package_dir) {
54        for entry in entries.flatten() {
55            let path = entry.path();
56            if path.is_dir()
57                && let Some(version_str) = path.file_name().and_then(|s| s.to_str())
58                && version_str != "latest"
59                && version_str != "dependents"
60                && Version::parse(version_str).is_ok()
61            {
62                versions.push(version_str.to_string());
63            }
64        }
65    }
66    versions.sort();
67
68    if versions.len() < 2 {
69        return Err(anyhow!("No previous version to roll back to."));
70    }
71
72    let current_version = versions
73        .pop()
74        .ok_or_else(|| anyhow!("Failed to get current version from list"))?;
75    let previous_version = versions
76        .pop()
77        .ok_or_else(|| anyhow!("Failed to get previous version from list"))?;
78
79    println!(
80        "Rolling back from version {} to {}",
81        current_version.to_string().yellow(),
82        previous_version.to_string().green()
83    );
84
85    if !core_utils::ask_for_confirmation("This will remove the current version. Continue?", yes) {
86        println!("Operation aborted.");
87        return Ok(());
88    }
89
90    let previous_version_dir = package_dir.join(&previous_version);
91
92    let manifest_filename = if let Some(sub) = &sub_package {
93        format!("manifest-{}.yaml", sub)
94    } else {
95        "manifest.yaml".to_string()
96    };
97    let prev_manifest_path = previous_version_dir.join(&manifest_filename);
98    if !prev_manifest_path.exists() {
99        return Err(anyhow!(
100            "No manifest found for {} in version {}. Rollback not possible.",
101            package_name,
102            previous_version
103        ));
104    }
105
106    let latest_symlink_path = package_dir.join("latest");
107    zoi_core::utils::symlink_dir(&previous_version_dir, &latest_symlink_path)?;
108
109    let content = fs::read_to_string(&prev_manifest_path)?;
110    let prev_manifest: types::InstallManifest = serde_yaml::from_str(&content)?;
111
112    if let Some(bins) = &prev_manifest.bins {
113        let bin_root = get_bin_root(scope)?;
114        for bin in bins {
115            let symlink_path = bin_root.join(bin);
116            create_shim(&symlink_path)?;
117        }
118    } else if prev_manifest.sub_package.is_none() {
119        let symlink_path = get_bin_root(scope)?.join(&current_manifest.name);
120        create_shim(&symlink_path)?;
121    }
122
123    if let Some(completions) = &prev_manifest.completions {
124        let prev_version_dir = package_dir.join(&previous_version);
125        for completion in completions {
126            let store_path = prev_version_dir
127                .join("shell")
128                .join(&completion.shell)
129                .join(&completion.filename);
130            let completions_root = super::get_completions_root(scope, &completion.shell)?;
131            let pkg_dir = completions_root.join(&prev_manifest.name);
132            let link_path = pkg_dir.join(&completion.filename);
133            if store_path.exists() {
134                let _ = super::create_completion_symlink(&store_path, &link_path);
135            }
136        }
137    }
138
139    let current_version_dir = package_dir.join(&current_version);
140    let mut has_other_manifests = false;
141    if let Ok(entries) = fs::read_dir(&current_version_dir) {
142        for entry in entries.flatten() {
143            let name = entry.file_name().to_string_lossy().to_string();
144            if name.starts_with("manifest") && name.ends_with(".yaml") && name != manifest_filename
145            {
146                has_other_manifests = true;
147                break;
148            }
149        }
150    }
151
152    if has_other_manifests {
153        for file_path_str in &current_manifest.installed_files {
154            let file_path = PathBuf::from(file_path_str);
155            if file_path.exists() {
156                if file_path.is_dir() {
157                    let _ = fs::remove_dir_all(&file_path);
158                } else {
159                    let _ = fs::remove_file(&file_path);
160                }
161            }
162        }
163        let current_manifest_path = current_version_dir.join(&manifest_filename);
164        if current_manifest_path.exists() {
165            fs::remove_file(current_manifest_path)?;
166        }
167    } else {
168        fs::remove_dir_all(current_version_dir)?;
169    }
170
171    println!(
172        "Successfully rolled back '{}' to version {}.",
173        package_name.cyan(),
174        previous_version.to_string().green()
175    );
176
177    Ok(())
178}
179
180fn get_bin_root(scope: types::Scope) -> Result<PathBuf> {
181    match scope {
182        types::Scope::User => {
183            let home_dir = core_utils::get_user_home()
184                .ok_or_else(|| anyhow!("Could not find home directory."))?;
185            Ok(sysroot::apply_sysroot(home_dir.join(".zoi/pkgs/bin")))
186        }
187        types::Scope::System => {
188            if cfg!(target_os = "windows") {
189                Ok(sysroot::apply_sysroot(PathBuf::from(
190                    "C:\\ProgramData\\zoi\\pkgs\\bin",
191                )))
192            } else {
193                Ok(sysroot::apply_sysroot(PathBuf::from("/usr/local/bin")))
194            }
195        }
196        types::Scope::Project => {
197            let current_dir = std::env::current_dir()?;
198            Ok(current_dir.join(".zoi").join("pkgs").join("bin"))
199        }
200    }
201}
202
203fn create_shim(path: &std::path::Path) -> Result<()> {
204    if cfg!(target_os = "windows") {
205        let binary_path = path.with_extension("exe");
206        if binary_path.exists() {
207            return Ok(());
208        }
209    } else if path.exists() {
210        return Ok(());
211    }
212    Ok(())
213}