1use clap_complete::ArgValueCompleter;
10use derive_more::From;
11use xvc_core::util::completer::strum_variants_completer;
12
13use std::collections::HashSet;
14
15use xvc_core::util::git::build_gitignore;
16use xvc_core::UpdateFromConfig;
17use xvc_core::{FromConfig, XvcConfigResult, XvcConfiguration};
18
19use xvc_core::XvcOutputSender;
20use xvc_core::{
21 ContentDigest, HashAlgorithm, TextOrBinary, XvcCachePath, XvcFileType, XvcMetadata, XvcRoot,
22};
23
24use crate::carry_in::carry_in;
25use crate::common::compare::{
26 diff_content_digest, diff_recheck_method, diff_text_or_binary, diff_xvc_path_metadata,
27};
28use crate::common::gitignore::{update_dir_gitignores, update_file_gitignores};
29use crate::common::{targets_from_disk, update_store_records, FileTextOrBinary};
30use crate::error::Result;
31
32use clap::Parser;
33use std::path::PathBuf;
34
35use xvc_core::RecheckMethod;
36use xvc_core::XvcPath;
37use xvc_core::{HStore, XvcEntity};
38
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, From, Parser)]
41#[command(rename_all = "kebab-case")]
42pub struct TrackCLI {
43 #[arg(long, alias = "as", add = ArgValueCompleter::new(strum_variants_completer::<RecheckMethod>))]
47 recheck_method: Option<RecheckMethod>,
48
49 #[arg(long)]
51 no_commit: bool,
52 #[arg(long, add = ArgValueCompleter::new(strum_variants_completer::<TextOrBinary>))]
55 text_or_binary: Option<FileTextOrBinary>,
56
57 #[arg(long)]
63 include_git_files: bool,
64
65 #[arg(long)]
67 force: bool,
68
69 #[arg(long)]
71 no_parallel: bool,
72
73 #[arg(value_hint=clap::ValueHint::AnyPath)]
75 targets: Option<Vec<String>>,
76}
77
78impl UpdateFromConfig for TrackCLI {
79 fn update_from_config(self, config: &XvcConfiguration) -> XvcConfigResult<Box<Self>> {
83 let recheck_method = self.recheck_method.unwrap_or_else(|| {
84 *RecheckMethod::from_config(config).expect("Configuration has a recheck method")
85 });
86
87 let no_commit = self.no_commit || config.file.track.no_commit;
88 let force = self.force || config.file.track.force;
89 let no_parallel = self.no_parallel || config.file.track.no_parallel;
90 let include_git_files = self.include_git_files || config.file.track.include_git_files;
91
92 let text_or_binary = Some(
93 self.text_or_binary
94 .unwrap_or_else(|| *FileTextOrBinary::from_config(config).unwrap()),
95 );
96
97 Ok(Box::new(Self {
98 targets: self.targets.clone(),
99 recheck_method: Some(recheck_method),
100 no_commit,
101 force,
102 no_parallel,
103 text_or_binary,
104 include_git_files,
105 }))
106 }
107}
108
109pub fn cmd_track(
132 output_snd: &XvcOutputSender,
133 xvc_root: &XvcRoot,
134 cli_opts: TrackCLI,
135) -> Result<()> {
136 let conf = xvc_root.config();
137 let opts = cli_opts.update_from_config(conf)?;
138 let current_dir = xvc_root.current_dir();
139 let filter_git_files = !opts.include_git_files;
140 let targets = targets_from_disk(
141 output_snd,
142 xvc_root,
143 current_dir,
144 &opts.targets,
145 filter_git_files,
146 )?;
147 let requested_recheck_method = opts.recheck_method;
148 let text_or_binary = opts.text_or_binary.unwrap_or_default();
149 let no_parallel = opts.no_parallel;
150
151 let stored_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
152 let stored_xvc_metadata_store = xvc_root.load_store::<XvcMetadata>()?;
153
154 let xvc_path_metadata_diff = diff_xvc_path_metadata(
155 xvc_root,
156 &stored_xvc_path_store,
157 &stored_xvc_metadata_store,
158 &targets,
159 );
160
161 let xvc_path_diff = xvc_path_metadata_diff.0;
162 let xvc_metadata_diff = xvc_path_metadata_diff.1;
163
164 let changed_entities: HashSet<XvcEntity> =
165 xvc_path_diff
166 .iter()
167 .filter_map(|(xe, xpd)| if xpd.changed() { Some(*xe) } else { None })
168 .chain(xvc_metadata_diff.iter().filter_map(|(xe, xpd)| {
169 if xpd.changed() {
170 Some(*xe)
171 } else {
172 None
173 }
174 }))
175 .collect();
176
177 let stored_recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
178 let default_recheck_method = *RecheckMethod::from_config(conf)?;
179 let recheck_method_diff = diff_recheck_method(
180 default_recheck_method,
181 &stored_recheck_method_store,
182 requested_recheck_method,
183 &changed_entities,
184 );
185
186 let stored_text_or_binary_store = xvc_root.load_store::<FileTextOrBinary>()?;
187 let text_or_binary_diff = diff_text_or_binary(
188 &stored_text_or_binary_store,
189 text_or_binary,
190 &changed_entities,
191 );
192
193 let hash_algorithm = *HashAlgorithm::from_config(conf)?;
194
195 let stored_content_digest_store = xvc_root.load_store::<ContentDigest>()?;
196
197 let content_digest_diff = diff_content_digest(
198 output_snd,
199 xvc_root,
200 &stored_xvc_path_store,
201 &stored_xvc_metadata_store,
202 &stored_content_digest_store,
203 &stored_text_or_binary_store,
204 &xvc_path_diff,
205 &xvc_metadata_diff,
206 opts.text_or_binary,
207 Some(hash_algorithm),
208 !no_parallel,
209 );
210
211 update_store_records(xvc_root, &xvc_path_diff, true, false)?;
212 update_store_records(xvc_root, &xvc_metadata_diff, true, false)?;
213 update_store_records(xvc_root, &recheck_method_diff, true, false)?;
214 update_store_records(xvc_root, &text_or_binary_diff, true, false)?;
215 update_store_records(xvc_root, &content_digest_diff, true, false)?;
216
217 let file_targets: Vec<XvcPath> = targets
218 .iter()
219 .filter_map(|(xp, xmd)| {
220 if xmd.file_type == XvcFileType::File {
221 Some(xp.clone())
222 } else {
223 None
224 }
225 })
226 .collect();
227
228 let dir_targets: Vec<XvcPath> = opts
231 .targets
232 .unwrap_or_else(|| vec![current_dir.to_string()])
233 .iter()
234 .filter_map(|t| {
235 let p = PathBuf::from(t);
236 if p.is_dir() {
237 XvcPath::new(xvc_root, current_dir, &p).ok()
238 } else {
239 None
240 }
241 })
242 .collect();
243
244 let current_gitignore = build_gitignore(xvc_root)?;
245
246 update_dir_gitignores(xvc_root, ¤t_gitignore, &dir_targets)?;
247 let current_gitignore = build_gitignore(xvc_root)?;
250
251 update_file_gitignores(xvc_root, ¤t_gitignore, &file_targets)?;
252
253 if !opts.no_commit {
254 let current_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
255
256 let updated_content_digest_store: HStore<ContentDigest> = content_digest_diff
257 .into_iter()
258 .filter_map(|(xe, cdd)| match cdd {
259 xvc_core::Diff::Identical => None,
260 xvc_core::Diff::RecordMissing { actual } => Some((xe, actual)),
261 xvc_core::Diff::ActualMissing { .. } => None,
262 xvc_core::Diff::Different { actual, .. } => Some((xe, actual)),
263 xvc_core::Diff::Skipped => None,
264 })
265 .collect();
266
267 let xvc_paths_to_carry =
270 current_xvc_path_store.subset(updated_content_digest_store.keys().cloned())?;
271
272 let xvc_paths_to_carry: HStore<XvcPath> = xvc_paths_to_carry
274 .into_iter()
275 .filter_map(|(xe, xp)| {
276 targets.get(&xp).and_then(|xmd| {
277 if xmd.file_type == XvcFileType::File {
278 Some((xe, xp))
279 } else {
280 None
281 }
282 })
283 })
284 .collect();
285
286 let cache_paths = updated_content_digest_store
287 .iter()
288 .filter_map(|(xe, cd)| {
289 xvc_paths_to_carry
290 .get(xe)
291 .and_then(|xp| XvcCachePath::new(xp, cd).ok())
292 .map(|cp| (*xe, cp))
293 })
294 .collect();
295
296 let recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
297
298 carry_in(
299 output_snd,
300 xvc_root,
301 &xvc_paths_to_carry,
302 &cache_paths,
303 &recheck_method_store,
304 !no_parallel,
305 opts.force,
306 )?;
307 }
308
309 Ok(())
310}