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 = local::manifest_filename(sub_package.as_deref());
66
67 let mut versions: Vec<String> = Vec::new();
68 if let Ok(entries) = fs::read_dir(&package_dir) {
69 for entry in entries.flatten() {
70 let name = entry.file_name().to_string_lossy().to_string();
71 if name != "latest"
72 && name != "dependents"
73 && name != current_version
74 {
75 versions.push(name);
76 }
77 }
78 }
79
80 if versions.is_empty() {
81 return Err(anyhow!(
82 "No previous versions found for package '{}'.",
83 current_manifest.name
84 ));
85 }
86
87 versions.sort_by(|a, b| {
88 let va = semver::Version::parse(a)
89 .unwrap_or_else(|_| semver::Version::new(0, 0, 0));
90 let vb = semver::Version::parse(b)
91 .unwrap_or_else(|_| semver::Version::new(0, 0, 0));
92 va.cmp(&vb)
93 });
94 let previous_version = versions.last().expect("Versions list is not empty");
95
96 println!(
97 "Rolling back from version {} to {}...",
98 current_version.yellow(),
99 previous_version.green()
100 );
101
102 let prev_version_dir = package_dir.join(previous_version);
103 let prev_manifest_path =
104 local::find_store_manifest(&prev_version_dir, sub_package.as_deref())
105 .or_else(|| {
106 let candidate = prev_version_dir.join(&manifest_filename);
108 candidate.exists().then_some(candidate)
109 });
110 let Some(prev_manifest_path) = prev_manifest_path else {
111 return Err(anyhow!(
112 "Previous manifest not found in: {}",
113 prev_version_dir.display()
114 ));
115 };
116
117 let prev_manifest = local::read_store_manifest(&prev_manifest_path)?;
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).extension().is_some_and(|ext| {
155 ext.eq_ignore_ascii_case("json")
156 || ext.eq_ignore_ascii_case("yaml")
157 })
158 && name != manifest_filename
159 {
160 has_other_manifests = true;
161 break;
162 }
163 }
164 }
165
166 if !has_other_manifests {
167 for file_path_str in ¤t_manifest.installed_files {
168 let expanded = core_utils::expand_placeholders(
171 file_path_str,
172 ¤t_version_dir,
173 scope
174 )?;
175 let file_path = PathBuf::from(expanded);
176 let Ok(meta) = fs::symlink_metadata(&file_path) else {
179 continue;
180 };
181 if meta.file_type().is_symlink() {
182 let _ = fs::remove_file(&file_path);
183 } else if meta.is_dir() {
184 if fs::read_dir(&file_path)
187 .is_ok_and(|mut entries| entries.next().is_none())
188 {
189 let _ = fs::remove_dir(&file_path);
190 }
191 } else {
192 let _ = fs::remove_file(&file_path);
193 }
194 }
195 }
196
197 let current_manifest_path = current_version_dir.join(&manifest_filename);
198 if current_manifest_path.exists() {
199 fs::remove_file(current_manifest_path)?;
200 }
201
202 if !has_other_manifests && current_version_dir.exists() {
203 let _ = fs::remove_dir_all(current_version_dir);
204 }
205
206 let latest_link = package_dir.join("latest");
207 if latest_link.exists() || latest_link.is_symlink() {
208 let _ = fs::remove_file(&latest_link);
209 }
210
211 #[cfg(unix)]
212 std::os::unix::fs::symlink(previous_version, latest_link)?;
213 #[cfg(windows)]
214 let _ = std::os::windows::fs::symlink_dir(previous_version, latest_link);
215
216 println!(
217 "{} Successfully rolled back {} to version {}.",
218 "::".bold().green(),
219 current_manifest.name.cyan(),
220 previous_version.green()
221 );
222
223 Ok(())
224}
225
226#[allow(dead_code)]
229fn find_binary_in_prev_manifest(_path: &std::path::Path) -> Option<PathBuf> {
230 None
231}
232
233fn create_shim(path: &std::path::Path) {
235 let _ = zoi_install::shim::create_shim(path);
236}