1use std::collections::{HashMap, HashSet};
5
6use crate::common::{cache_paths_for_xvc_paths, filter_targets_from_store};
7use crate::Result;
8
9use clap::Parser;
10use clap_complete::ArgValueCompleter;
11use itertools::Itertools;
12
13use xvc_core::types::xvcdigest::DIGEST_LENGTH;
14use xvc_core::util::completer::xvc_path_completer;
15use xvc_core::{XvcCachePath, XvcRoot};
16use xvc_core::XvcEntity;
17use xvc_core::{output, uwr, warn, XvcOutputSender};
18use xvc_storage::storage::{get_storage_record, storage_identifier_completer};
19use xvc_storage::{StorageIdentifier, XvcStorageOperations};
20
21#[derive(Debug, Clone, PartialEq, Eq, Parser)]
23#[command(rename_all = "kebab-case", author, version)]
24pub struct RemoveCLI {
25 #[arg(long, required_unless_present = "from_storage")]
27 from_cache: bool,
28
29 #[arg(long, required_unless_present = "from_cache", add = ArgValueCompleter::new(storage_identifier_completer))]
31 from_storage: Option<StorageIdentifier>,
32
33 #[arg(long)]
35 all_versions: bool,
36
37 #[arg(long, conflicts_with = "all_versions")]
43 only_version: Option<String>,
44
45 #[arg(long)]
47 force: bool,
48
49 #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
51 targets: Vec<String>,
52}
53
54pub fn cmd_remove(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, opts: RemoveCLI) -> Result<()> {
56 if !opts.from_cache && opts.from_storage.is_none() {
57 return Err(anyhow::anyhow!(
58 "At least one of --from-cache or --from-storage must be specified"
59 )
60 .into());
61 }
62
63 if opts.all_versions && opts.only_version.is_some() {
64 return Err(
65 anyhow::anyhow!("Cannot specify both --all-versions and --only-version").into(),
66 );
67 }
68
69 let current_dir = xvc_root.config().current_dir()?;
70
71 let all_paths = xvc_root.load_store()?;
72 let all_content_digests = xvc_root.load_store()?;
73 let remove_targets = filter_targets_from_store(
74 output_snd,
75 xvc_root,
76 &all_paths,
77 current_dir,
78 &Some(opts.targets),
79 )?;
80
81 let all_cache_paths = cache_paths_for_xvc_paths(output_snd, &all_paths, &all_content_digests)?;
82
83 let cache_paths_for_targets = all_cache_paths.subset(remove_targets.keys().copied())?;
84 let candidate_paths = if opts.all_versions {
85 cache_paths_for_targets
87 .iter()
88 .flat_map(|(xe, vec_cp)| {
89 vec_cp
90 .iter()
91 .map(|cp| (*xe, cp.clone()))
92 .collect::<Vec<_>>()
93 })
94 .collect::<Vec<_>>()
95 } else if let Some(version) = opts.only_version {
96 let version_cmp_str = version.replace('-', "");
98 let version_cmp = |v: &&XvcCachePath| {
99 let digest_str = v.digest_string(DIGEST_LENGTH).replace('-', "");
100 digest_str[2..].starts_with(&version_cmp_str)
102 };
103 let paths = cache_paths_for_targets
104 .iter()
105 .filter_map(|(xe, vec_cp)| {
106 let possible_paths = vec_cp
107 .iter()
108 .filter(version_cmp)
109 .cloned()
110 .collect::<Vec<XvcCachePath>>();
111 if !possible_paths.is_empty() {
112 Some((*xe, possible_paths))
113 } else {
114 None
115 }
116 })
117 .fold(
118 Vec::<(XvcEntity, XvcCachePath)>::new(),
119 |mut acc, (xe, vec_cp)| {
120 vec_cp.into_iter().for_each(|xcp| acc.push((xe, xcp)));
121 acc
122 },
123 );
124
125 if paths.len() > 1 {
126 return Err(anyhow::anyhow!(
127 "Version prefix is not unique:\n{}",
128 paths
129 .iter()
130 .map(|(_, xcp)| xcp.digest_string(DIGEST_LENGTH))
131 .join("\n")
132 )
133 .into());
134 } else {
135 paths
136 }
137 } else {
138 remove_targets
139 .iter()
140 .filter_map(|(xe, xp)| {
141 all_content_digests
142 .get(xe)
143 .map(|cd| (*xe, XvcCachePath::new(xp, cd).unwrap()))
144 })
145 .collect::<Vec<(XvcEntity, XvcCachePath)>>()
146 };
147
148 let mut entities_for_cache_path: HashMap<XvcCachePath, HashSet<XvcEntity>> = HashMap::new();
149
150 for (xe, cache_paths) in all_cache_paths.iter() {
151 for cp in cache_paths {
152 if !entities_for_cache_path.contains_key(cp) {
153 entities_for_cache_path.insert(cp.clone(), HashSet::new());
154 }
155 let entity_set = entities_for_cache_path.get_mut(cp).unwrap();
156 entity_set.insert(*xe);
157 }
158 }
159
160 let mut deletable_paths = Vec::<XvcCachePath>::new();
161 let removable_entities: HashSet<XvcEntity> = remove_targets.keys().copied().collect();
163 for (xe, cp) in candidate_paths {
164 let entities_pointing_to_cp =
165 HashSet::from_iter(entities_for_cache_path[&cp].iter().copied());
166 let mut deletable = true;
167 entities_pointing_to_cp
168 .difference(&removable_entities)
169 .for_each(|other_xe| {
170 let this_xp = all_paths.get(&xe).unwrap();
171 let other_xp = all_paths.get(other_xe).unwrap();
172 if opts.force {
173 warn!(
174 output_snd,
175 "Deleting {} (for {}) even though it's also used by {}!",
176 cp,
177 this_xp,
178 other_xp
179 );
180 } else {
181 output!(
182 output_snd,
183 "Not deleting {} (for {}) because it's also used by {}",
184 cp,
185 this_xp,
186 other_xp
187 );
188 deletable = false;
189 }
190 });
191
192 if deletable {
193 deletable_paths.push(cp);
194 }
195 }
196
197 deletable_paths.sort_unstable();
199
200 if opts.from_cache {
201 deletable_paths
202 .iter()
203 .for_each(|xcp| uwr!(xcp.remove(output_snd, xvc_root), output_snd));
204 }
205
206 if let Some(storage) = opts.from_storage {
207 let storage = get_storage_record(output_snd, xvc_root, &storage)?;
208 storage.delete(output_snd, xvc_root, deletable_paths.as_slice())?;
209 }
210
211 Ok(())
212}