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::{FromConfigKey, XvcConfigResult};
17use xvc_core::{UpdateFromXvcConfig, XvcConfig};
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 UpdateFromXvcConfig for TrackCLI {
79 fn update_from_conf(self, conf: &XvcConfig) -> XvcConfigResult<Box<Self>> {
83 let recheck_method = self
84 .recheck_method
85 .unwrap_or_else(|| RecheckMethod::from_conf(conf));
86 let no_commit = self.no_commit || conf.get_bool("file.track.no_commit")?.option;
87 let force = self.force || conf.get_bool("file.track.force")?.option;
88 let no_parallel = self.no_parallel || conf.get_bool("file.track.no_parallel")?.option;
89 let text_or_binary = self.text_or_binary.as_ref().map_or_else(
90 || Some(FileTextOrBinary::from_conf(conf)),
91 |v| Some(v.to_owned()),
92 );
93 let include_git_files =
94 self.include_git_files || conf.get_bool("file.track.include_git_files")?.option;
95
96 Ok(Box::new(Self {
97 targets: self.targets.clone(),
98 recheck_method: Some(recheck_method),
99 no_commit,
100 force,
101 no_parallel,
102 text_or_binary,
103 include_git_files,
104 }))
105 }
106}
107
108pub fn cmd_track(
131 output_snd: &XvcOutputSender,
132 xvc_root: &XvcRoot,
133 cli_opts: TrackCLI,
134) -> Result<()> {
135 let conf = xvc_root.config();
136 let opts = cli_opts.update_from_conf(conf)?;
137 let current_dir = conf.current_dir()?;
138 let filter_git_files = !opts.include_git_files;
139 let targets = targets_from_disk(
140 output_snd,
141 xvc_root,
142 current_dir,
143 &opts.targets,
144 filter_git_files,
145 )?;
146 let requested_recheck_method = opts.recheck_method;
147 let text_or_binary = opts.text_or_binary.unwrap_or_default();
148 let no_parallel = opts.no_parallel;
149
150 let stored_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
151 let stored_xvc_metadata_store = xvc_root.load_store::<XvcMetadata>()?;
152
153 let xvc_path_metadata_diff = diff_xvc_path_metadata(
154 xvc_root,
155 &stored_xvc_path_store,
156 &stored_xvc_metadata_store,
157 &targets,
158 );
159
160 let xvc_path_diff = xvc_path_metadata_diff.0;
161 let xvc_metadata_diff = xvc_path_metadata_diff.1;
162
163 let changed_entities: HashSet<XvcEntity> =
164 xvc_path_diff
165 .iter()
166 .filter_map(|(xe, xpd)| if xpd.changed() { Some(*xe) } else { None })
167 .chain(xvc_metadata_diff.iter().filter_map(|(xe, xpd)| {
168 if xpd.changed() {
169 Some(*xe)
170 } else {
171 None
172 }
173 }))
174 .collect();
175
176 let stored_recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
177 let default_recheck_method = RecheckMethod::from_conf(conf);
178 let recheck_method_diff = diff_recheck_method(
179 default_recheck_method,
180 &stored_recheck_method_store,
181 requested_recheck_method,
182 &changed_entities,
183 );
184
185 let stored_text_or_binary_store = xvc_root.load_store::<FileTextOrBinary>()?;
186 let text_or_binary_diff = diff_text_or_binary(
187 &stored_text_or_binary_store,
188 text_or_binary,
189 &changed_entities,
190 );
191
192 let hash_algorithm = HashAlgorithm::from_conf(conf);
193
194 let stored_content_digest_store = xvc_root.load_store::<ContentDigest>()?;
195
196 let content_digest_diff = diff_content_digest(
197 output_snd,
198 xvc_root,
199 &stored_xvc_path_store,
200 &stored_xvc_metadata_store,
201 &stored_content_digest_store,
202 &stored_text_or_binary_store,
203 &xvc_path_diff,
204 &xvc_metadata_diff,
205 opts.text_or_binary,
206 Some(hash_algorithm),
207 !no_parallel,
208 );
209
210 update_store_records(xvc_root, &xvc_path_diff, true, false)?;
211 update_store_records(xvc_root, &xvc_metadata_diff, true, false)?;
212 update_store_records(xvc_root, &recheck_method_diff, true, false)?;
213 update_store_records(xvc_root, &text_or_binary_diff, true, false)?;
214 update_store_records(xvc_root, &content_digest_diff, true, false)?;
215
216 let file_targets: Vec<XvcPath> = targets
217 .iter()
218 .filter_map(|(xp, xmd)| {
219 if xmd.file_type == XvcFileType::File {
220 Some(xp.clone())
221 } else {
222 None
223 }
224 })
225 .collect();
226
227 let dir_targets: Vec<XvcPath> = opts
230 .targets
231 .unwrap_or_else(|| vec![current_dir.to_string()])
232 .iter()
233 .filter_map(|t| {
234 let p = PathBuf::from(t);
235 if p.is_dir() {
236 XvcPath::new(xvc_root, current_dir, &p).ok()
237 } else {
238 None
239 }
240 })
241 .collect();
242
243 let current_gitignore = build_gitignore(xvc_root)?;
244
245 update_dir_gitignores(xvc_root, ¤t_gitignore, &dir_targets)?;
246 let current_gitignore = build_gitignore(xvc_root)?;
249
250 update_file_gitignores(xvc_root, ¤t_gitignore, &file_targets)?;
251
252 if !opts.no_commit {
253 let current_xvc_path_store = xvc_root.load_store::<XvcPath>()?;
254
255 let updated_content_digest_store: HStore<ContentDigest> = content_digest_diff
256 .into_iter()
257 .filter_map(|(xe, cdd)| match cdd {
258 xvc_core::Diff::Identical => None,
259 xvc_core::Diff::RecordMissing { actual } => Some((xe, actual)),
260 xvc_core::Diff::ActualMissing { .. } => None,
261 xvc_core::Diff::Different { actual, .. } => Some((xe, actual)),
262 xvc_core::Diff::Skipped => None,
263 })
264 .collect();
265
266 let xvc_paths_to_carry =
269 current_xvc_path_store.subset(updated_content_digest_store.keys().cloned())?;
270
271 let xvc_paths_to_carry: HStore<XvcPath> = xvc_paths_to_carry
273 .into_iter()
274 .filter_map(|(xe, xp)| {
275 targets.get(&xp).and_then(|xmd| {
276 if xmd.file_type == XvcFileType::File {
277 Some((xe, xp))
278 } else {
279 None
280 }
281 })
282 })
283 .collect();
284
285 let cache_paths = updated_content_digest_store
286 .iter()
287 .filter_map(|(xe, cd)| {
288 xvc_paths_to_carry
289 .get(xe)
290 .and_then(|xp| XvcCachePath::new(xp, cd).ok())
291 .map(|cp| (*xe, cp))
292 })
293 .collect();
294
295 let recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
296
297 carry_in(
298 output_snd,
299 xvc_root,
300 &xvc_paths_to_carry,
301 &cache_paths,
302 &recheck_method_store,
303 !no_parallel,
304 opts.force,
305 )?;
306 }
307
308 Ok(())
309}