zoi_transaction/
rollback.rs1use std::fs;
4use std::path::PathBuf;
5
6use anyhow::{Result, anyhow};
7use colored::Colorize;
8use zoi_core::{types, utils as core_utils};
9use zoi_resolver::{local, resolve};
10
11pub fn run(package_name: &str, yes: bool) -> Result<()> {
19 println!("Attempting to roll back '{}'...", package_name.cyan());
20
21 let request = resolve::parse_source_string(package_name)?;
22 let sub_package = request.sub_package.clone();
23
24 let scope_order = [
25 types::Scope::User,
26 types::Scope::System,
27 types::Scope::Project
28 ];
29 let mut current_manifest = None;
30 let mut scope = None;
31 for candidate_scope in scope_order {
32 let mut matches = local::find_installed_manifests_matching(
33 &request,
34 candidate_scope
35 )?;
36 match matches.len() {
37 0 => {}
38 1 => {
39 current_manifest = Some(matches.remove(0));
40 scope = Some(candidate_scope);
41 break;
42 }
43 _ => {
44 return Err(anyhow!(
45 "Ambiguous package name '{package_name}' matches multiple \
46 installed packages.",
47 ));
48 }
49 }
50 }
51
52 let Some(current_manifest) = current_manifest else {
53 return Err(anyhow!("Package '{package_name}' is not installed."));
54 };
55 let scope = scope.expect("Scope should be resolved if manifest is found");
56
57 let package_dir = local::get_package_dir(
58 scope,
59 ¤t_manifest.registry_handle,
60 ¤t_manifest.repo,
61 ¤t_manifest.name
62 )?;
63
64 let current_version = current_manifest.version.clone();
65 let manifest_filename = if let Some(sub) = &sub_package {
66 format!("manifest-{sub}.yaml")
67 } else {
68 "manifest.yaml".to_string()
69 };
70
71 let mut versions: Vec<String> = Vec::new();
72 if let Ok(entries) = fs::read_dir(&package_dir) {
73 for entry in entries.flatten() {
74 let name = entry.file_name().to_string_lossy().to_string();
75 if name != "latest"
76 && name != "dependents"
77 && name != current_version
78 {
79 versions.push(name);
80 }
81 }
82 }
83
84 if versions.is_empty() {
85 return Err(anyhow!(
86 "No previous versions found for package '{}'.",
87 current_manifest.name
88 ));
89 }
90
91 versions.sort_by(|a, b| {
92 let va = semver::Version::parse(a)
93 .unwrap_or_else(|_| semver::Version::new(0, 0, 0));
94 let vb = semver::Version::parse(b)
95 .unwrap_or_else(|_| semver::Version::new(0, 0, 0));
96 va.cmp(&vb)
97 });
98 let previous_version = versions.last().expect("Versions list is not empty");
99
100 println!(
101 "Rolling back from version {} to {}...",
102 current_version.yellow(),
103 previous_version.green()
104 );
105
106 let prev_manifest_path =
107 package_dir.join(previous_version).join(&manifest_filename);
108 if !prev_manifest_path.exists() {
109 return Err(anyhow!(
110 "Previous manifest not found at: {}",
111 prev_manifest_path.display()
112 ));
113 }
114
115 let prev_manifest_content = fs::read_to_string(&prev_manifest_path)?;
116 let prev_manifest: types::InstallManifest =
117 serde_yaml::from_str(&prev_manifest_content)?;
118
119 if !yes
120 && !core_utils::ask_for_confirmation("Do you want to proceed?", false)
121 {
122 return Err(anyhow!("Rollback aborted by user."));
123 }
124
125 for file_path_str in &prev_manifest.installed_files {
126 let file_path = std::path::Path::new(file_path_str);
127 create_shim(file_path);
128 }
129
130 if let Some(completions) = &prev_manifest.completions {
131 for completion in completions {
132 let store_path = package_dir
133 .join(previous_version)
134 .join("data/shell")
135 .join(&completion.shell)
136 .join(&completion.filename);
137 let completions_root =
138 super::get_completions_root(scope, &completion.shell)?;
139 let pkg_dir = completions_root.join(&prev_manifest.name);
140 let link_path = pkg_dir.join(&completion.filename);
141 if store_path.exists() {
142 let _ =
143 super::create_completion_symlink(&store_path, &link_path);
144 }
145 }
146 }
147
148 let current_version_dir = package_dir.join(¤t_version);
149 let mut has_other_manifests = false;
150 if let Ok(entries) = fs::read_dir(¤t_version_dir) {
151 for entry in entries.flatten() {
152 let name = entry.file_name().to_string_lossy().to_string();
153 if name.starts_with("manifest")
154 && std::path::Path::new(&name)
155 .extension()
156 .is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
157 && name != manifest_filename
158 {
159 has_other_manifests = true;
160 break;
161 }
162 }
163 }
164
165 if !has_other_manifests {
166 for file_path_str in ¤t_manifest.installed_files {
167 let file_path = std::path::Path::new(file_path_str);
168 if file_path.exists() {
169 if file_path.is_dir() {
170 let _ = fs::remove_dir_all(file_path);
171 } else {
172 let _ = fs::remove_file(file_path);
173 }
174 }
175 }
176 }
177
178 let current_manifest_path = current_version_dir.join(&manifest_filename);
179 if current_manifest_path.exists() {
180 fs::remove_file(current_manifest_path)?;
181 }
182
183 if !has_other_manifests && current_version_dir.exists() {
184 let _ = fs::remove_dir_all(current_version_dir);
185 }
186
187 let latest_link = package_dir.join("latest");
188 if latest_link.exists() || latest_link.is_symlink() {
189 let _ = fs::remove_file(&latest_link);
190 }
191
192 #[cfg(unix)]
193 std::os::unix::fs::symlink(previous_version, latest_link)?;
194 #[cfg(windows)]
195 let _ = std::os::windows::fs::symlink_dir(previous_version, latest_link);
196
197 println!(
198 "{} Successfully rolled back {} to version {}.",
199 "::".bold().green(),
200 current_manifest.name.cyan(),
201 previous_version.green()
202 );
203
204 Ok(())
205}
206
207#[allow(dead_code)]
210fn find_binary_in_prev_manifest(_path: &std::path::Path) -> Option<PathBuf> {
211 None
212}
213
214fn create_shim(path: &std::path::Path) {
216 let _ = zoi_install::shim::create_shim(path);
217}