1use crate::common::{load_targets_from_store, targets_from_disk, FileTextOrBinary};
7use crate::error::Error;
8use crate::Result;
9use anyhow::anyhow;
10use chrono;
11use clap::Parser;
12use clap_complete::ArgValueCompleter;
13use xvc_core::util::completer::{strum_variants_completer, xvc_path_completer};
14
15use std::collections::{HashMap, HashSet};
16use std::fmt::Display;
17use std::fmt::Formatter;
18use std::path::Path;
19use std::str::FromStr;
20use std::time::SystemTime;
21use strum_macros::{Display as EnumDisplay, EnumString, VariantNames};
22use xvc_core::types::xvcdigest::DIGEST_LENGTH;
23use xvc_core::{conf, FromConfigKey, UpdateFromXvcConfig};
24use xvc_core::{error, output, XvcOutputSender};
25use xvc_core::{
26 ContentDigest, HashAlgorithm, RecheckMethod, XvcConfigResult, XvcFileType, XvcMetadata,
27 XvcPath, XvcRoot,
28};
29use xvc_core::{HStore, XvcEntity, XvcStore};
30
31#[derive(Debug, Clone, EnumString, EnumDisplay, PartialEq, Eq)]
33pub enum ListColumn {
34 #[strum(serialize = "acd64")]
36 ActualContentDigest64,
37
38 #[strum(serialize = "acd8")]
40 ActualContentDigest8,
41
42 #[strum(serialize = "aft")]
44 ActualFileType,
45
46 #[strum(serialize = "asz")]
48 ActualSize,
49
50 #[strum(serialize = "ats")]
52 ActualTimestamp,
53
54 #[strum(serialize = "name")]
56 Name,
57
58 #[strum(serialize = "cst")]
60 CacheStatus,
61
62 #[strum(serialize = "rrm")]
64 RecordedRecheckMethod,
65
66 #[strum(serialize = "rcd64")]
68 RecordedContentDigest64,
69
70 #[strum(serialize = "rcd8")]
72 RecordedContentDigest8,
73
74 #[strum(serialize = "rsz")]
76 RecordedSize,
77
78 #[strum(serialize = "rts")]
80 RecordedTimestamp,
81
82 #[strum(disabled)]
84 Literal(String),
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct ListFormat {
90 pub columns: Vec<ListColumn>,
92}
93
94impl FromStr for ListFormat {
95 type Err = crate::Error;
96
97 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
98 let mut columns = Vec::new();
99 for begin_marker in s.split("{{") {
100 let items = begin_marker.split("}}");
101 for item in items {
102 if let Ok(col) = item.parse::<ListColumn>() {
103 columns.push(col);
104 } else {
105 columns.push(ListColumn::Literal(item.to_string()));
106 }
107 }
108 }
109 Ok(Self { columns })
110 }
111}
112
113conf!(ListFormat, "file.list.format");
114
115#[derive(Debug, Copy, Clone, EnumString, EnumDisplay, PartialEq, Eq, VariantNames)]
117pub enum ListSortCriteria {
118 #[strum(serialize = "none")]
119 None,
121 #[strum(serialize = "name-asc")]
122 NameAsc,
124 #[strum(serialize = "name-desc")]
125 NameDesc,
127 #[strum(serialize = "size-asc")]
128 SizeAsc,
130 #[strum(serialize = "size-desc")]
131 SizeDesc,
133 #[strum(serialize = "t-asc", serialize = "timestamp-asc", serialize = "ts-asc")]
134 TimestampAsc,
136 #[strum(
137 serialize = "t-desc",
138 serialize = "timestamp-desc",
139 serialize = "ts-desc"
140 )]
141 TimestampDesc,
143}
144conf!(ListSortCriteria, "file.list.sort");
145
146#[derive(Debug, Clone, PartialEq)]
148pub struct ListRow {
149 pub actual_content_digest_str: String,
151 pub actual_size: u64,
153 pub actual_size_str: String,
155 pub actual_timestamp: SystemTime,
157 pub actual_timestamp_str: String,
159 pub actual_file_type: String,
161
162 pub name: String,
164 pub cache_status: String,
166
167 pub recorded_recheck_method: String,
169 pub recorded_content_digest_str: String,
171 pub recorded_size: u64,
173 pub recorded_size_str: String,
175 #[allow(dead_code)]
178 pub recorded_timestamp: SystemTime,
179 pub recorded_timestamp_str: String,
181}
182
183impl ListRow {
184 fn new(path_prefix: &Path, path_match: PathMatch) -> Result<Self> {
185 let actual_file_type =
186 String::from(if let Some(actual_metadata) = path_match.actual_metadata {
187 match actual_metadata.file_type {
188 XvcFileType::Missing => "X",
189 XvcFileType::File => "F",
190 XvcFileType::Directory => "D",
191 XvcFileType::Symlink => "S",
192 XvcFileType::Hardlink => "H",
193 XvcFileType::Reflink => "R",
194 }
195 } else {
196 "X"
197 });
198
199 let recorded_recheck_method =
200 if let Some(recheck_method) = path_match.recorded_recheck_method {
201 format_recheck_method(recheck_method)
202 } else {
203 "X".to_string()
204 };
205
206 let actual_content_digest_str = match path_match.actual_digest {
207 Some(digest) => format!("{}", digest),
208 None => str::repeat(" ", DIGEST_LENGTH * 2),
209 };
210
211 let actual_size = path_match
212 .actual_metadata
213 .and_then(|md| md.size)
214 .unwrap_or(0);
215
216 let actual_size_str = format_size(path_match.actual_metadata.and_then(|md| md.size));
217
218 let actual_timestamp = path_match
219 .actual_metadata
220 .and_then(|md| md.modified)
221 .unwrap_or(SystemTime::UNIX_EPOCH);
222
223 let actual_timestamp_str =
224 format_timestamp(path_match.actual_metadata.and_then(|md| md.modified));
225
226 let recorded_size = path_match
227 .recorded_metadata
228 .and_then(|md| md.size)
229 .unwrap_or(0);
230
231 let recorded_size_str = format_size(path_match.recorded_metadata.and_then(|md| md.size));
232
233 let recorded_timestamp = path_match
234 .recorded_metadata
235 .and_then(|md| md.modified)
236 .unwrap_or(SystemTime::UNIX_EPOCH);
237
238 let recorded_timestamp_str =
239 format_timestamp(path_match.recorded_metadata.and_then(|md| md.modified));
240
241 let recorded_content_digest_str = match path_match.recorded_digest {
242 Some(digest) => format!("{}", digest),
243 None => str::repeat(" ", DIGEST_LENGTH * 2),
244 };
245
246 let name = if let Some(ap) = path_match.actual_path {
247 ap.strip_prefix(path_prefix.to_string_lossy().as_ref())
248 .map_err(|e| Error::RelativeStripPrefixError { e })?
249 .to_string()
250 } else if let Some(rp) = path_match.recorded_path {
251 rp.strip_prefix(path_prefix.to_string_lossy().as_ref())
252 .map_err(|e| Error::RelativeStripPrefixError { e })?
253 .to_string()
254 } else {
255 return Err(anyhow!("No actual or recorded path for {:?}", path_match).into());
256 };
257
258 let cache_status = if path_match.actual_metadata.is_some() {
260 if path_match.recorded_metadata.is_some() {
261 let actual_secs = actual_timestamp
263 .duration_since(SystemTime::UNIX_EPOCH)?
264 .as_secs();
265 let recorded_secs = recorded_timestamp
266 .duration_since(SystemTime::UNIX_EPOCH)?
267 .as_secs();
268 match actual_secs.cmp(&recorded_secs) {
269 std::cmp::Ordering::Less => "<".to_string(),
270 std::cmp::Ordering::Greater => ">".to_string(),
271 std::cmp::Ordering::Equal => "=".to_string(),
272 }
273 } else {
274 "X".to_string()
275 }
276 } else {
277 "?".to_string()
278 };
279
280 Ok(ListRow {
281 actual_content_digest_str,
282 actual_file_type,
283 actual_size,
284 actual_size_str,
285 actual_timestamp,
286 actual_timestamp_str,
287 name,
288 cache_status,
289 recorded_recheck_method,
290 recorded_content_digest_str,
291 recorded_size,
292 recorded_size_str,
293 recorded_timestamp,
294 recorded_timestamp_str,
295 })
296 }
297}
298
299fn format_recheck_method(recheck_method: RecheckMethod) -> String {
300 match recheck_method {
301 RecheckMethod::Copy => "C".to_string(),
302 RecheckMethod::Symlink => "S".to_string(),
303 RecheckMethod::Hardlink => "H".to_string(),
304 RecheckMethod::Reflink => "R".to_string(),
305 }
306}
307
308pub fn format_timestamp(timestamp: Option<SystemTime>) -> String {
311 match timestamp {
312 Some(timestamp) => {
313 let timestamp = chrono::DateTime::<chrono::Utc>::from(timestamp);
314 format!("{}", timestamp.format("%Y-%m-%d %H:%M:%S"))
315 }
316 None => " ".to_string(),
317 }
318}
319
320pub fn format_size(size: Option<u64>) -> String {
329 match size {
330 Some(size) => {
331 if size < 1024 * 1024 {
332 format!("{:11}", size)
333 } else if size < 1024 * 1024 * 1024 {
334 format!("{:>4}MB.{}", size / 1024 / 1024, size % 1000)
335 } else if size < 1024 * 1024 * 1024 * 1024 {
336 format!("{:>4}GB.{}", size / 1024 / 1024 / 1024, size % 1000)
337 } else {
338 format!("{:>4}TB.{}", size / 1024 / 1024 / 1024 / 1024, size % 1000)
339 }
340 }
341 None => " ".to_owned(),
342 }
343}
344
345#[derive(Debug, Clone)]
346struct PathMatch {
347 xvc_entity: Option<XvcEntity>,
348 actual_path: Option<XvcPath>,
349 actual_metadata: Option<XvcMetadata>,
350 actual_digest: Option<ContentDigest>,
351 recorded_path: Option<XvcPath>,
352 recorded_metadata: Option<XvcMetadata>,
353 recorded_digest: Option<ContentDigest>,
354 recorded_recheck_method: Option<RecheckMethod>,
355}
356
357#[derive(Debug, Clone, PartialEq)]
359pub struct ListRows {
360 pub format: ListFormat,
362 pub sort_criteria: ListSortCriteria,
364 pub rows: Vec<ListRow>,
366}
367
368impl ListRows {
369 pub fn new(format: ListFormat, sort_criteria: ListSortCriteria, rows: Vec<ListRow>) -> Self {
371 let mut s = Self {
372 format,
373 sort_criteria,
374 rows,
375 };
376 sort_list_rows(&mut s);
377 s
378 }
379
380 pub fn empty() -> Self {
382 Self {
383 format: ListFormat { columns: vec![] },
384 sort_criteria: ListSortCriteria::None,
385 rows: vec![],
386 }
387 }
388
389 pub fn total_lines(&self) -> usize {
391 self.rows.len()
392 }
393
394 pub fn total_actual_size(&self) -> u64 {
396 self.rows.iter().fold(0u64, |tot, r| tot + r.actual_size)
397 }
398
399 pub fn total_cached_size(&self) -> u64 {
401 let mut cached_sizes = HashMap::<String, u64>::new();
402 self.rows.iter().for_each(|r| {
403 if !r.recorded_content_digest_str.trim().is_empty() {
404 cached_sizes.insert(r.recorded_content_digest_str.to_string(), r.recorded_size);
405 }
406 });
407
408 cached_sizes.values().sum()
409 }
410}
411
412pub fn build_row(row: &ListRow, format: &ListFormat) -> String {
414 let mut output = String::new();
415 for column in &format.columns {
416 match column {
417 ListColumn::RecordedRecheckMethod => output.push_str(&row.recorded_recheck_method),
418 ListColumn::ActualFileType => output.push_str(&row.actual_file_type),
419 ListColumn::ActualSize => output.push_str(&row.actual_size_str),
420 ListColumn::ActualContentDigest64 => output.push_str(&row.actual_content_digest_str),
421 ListColumn::ActualContentDigest8 => {
422 output.push_str(if row.actual_content_digest_str.len() >= 8 {
423 &row.actual_content_digest_str[..8]
424 } else {
425 &row.actual_content_digest_str
426 })
427 }
428 ListColumn::ActualTimestamp => output.push_str(&row.actual_timestamp_str),
429 ListColumn::Name => output.push_str(&row.name),
430 ListColumn::RecordedSize => output.push_str(&row.recorded_size_str),
431 ListColumn::RecordedContentDigest64 => {
432 output.push_str(&row.recorded_content_digest_str)
433 }
434 ListColumn::RecordedContentDigest8 => {
435 output.push_str(if row.recorded_content_digest_str.len() >= 8 {
436 &row.recorded_content_digest_str[..8]
437 } else {
438 &row.recorded_content_digest_str
439 })
440 }
441 ListColumn::RecordedTimestamp => output.push_str(&row.recorded_timestamp_str),
442 ListColumn::CacheStatus => output.push_str(&row.cache_status),
443 ListColumn::Literal(literal) => output.push_str(literal),
444 }
445 }
446 output
447}
448
449type BuildRowFn = Box<dyn Fn(&ListRow, &ListFormat) -> String>;
451
452pub fn build_table(list_rows: &ListRows, build_row: BuildRowFn) -> String {
454 let mut output = String::new();
455
456 let format = &list_rows.format;
457 for row in list_rows.rows.iter() {
458 let row_str = build_row(row, format);
459 output.push_str(&row_str);
460 output.push('\n');
461 }
462
463 output
464}
465
466fn add_summary_line(list_rows: &ListRows) -> String {
467 let total_lines = list_rows.total_lines();
468 let total_actual_size = format_size(Some(list_rows.total_actual_size()));
469 let total_cached_size = format_size(Some(list_rows.total_cached_size()));
470
471 format!("Total #: {total_lines} Workspace Size: {total_actual_size} Cached Size: {total_cached_size}\n")
473}
474
475fn sort_list_rows(list_rows: &mut ListRows) {
476 let row_cmp = match list_rows.sort_criteria {
477 ListSortCriteria::NameAsc => |a: &ListRow, b: &ListRow| a.name.cmp(&b.name),
478 ListSortCriteria::NameDesc => |a: &ListRow, b: &ListRow| b.name.cmp(&a.name),
479 ListSortCriteria::SizeAsc => |a: &ListRow, b: &ListRow| a.actual_size.cmp(&b.actual_size),
480 ListSortCriteria::SizeDesc => |a: &ListRow, b: &ListRow| b.actual_size.cmp(&a.actual_size),
481 ListSortCriteria::TimestampAsc => {
482 |a: &ListRow, b: &ListRow| a.actual_timestamp.cmp(&b.actual_timestamp)
483 }
484 ListSortCriteria::TimestampDesc => {
485 |a: &ListRow, b: &ListRow| b.actual_timestamp.cmp(&a.actual_timestamp)
486 }
487 ListSortCriteria::None => |_: &ListRow, _: &ListRow| std::cmp::Ordering::Equal,
488 };
489
490 list_rows.rows.sort_unstable_by(row_cmp);
491}
492
493impl Display for ListRows {
494 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
495 write!(f, "{}", build_table(self, Box::new(build_row)))?;
496 Ok(())
497 }
498}
499
500#[derive(Debug, Clone, PartialEq, Eq, Parser)]
516#[command(rename_all = "kebab-case")]
517pub struct ListCLI {
518 #[arg(long, short = 'f', verbatim_doc_comment)]
545 pub format: Option<ListFormat>,
546 #[arg(long, short = 's', add = ArgValueCompleter::new(strum_variants_completer::<ListSortCriteria>))]
552 pub sort: Option<ListSortCriteria>,
553
554 #[arg(long)]
558 pub no_summary: bool,
559
560 #[arg(long, short = 'd', aliases=&["show-dirs"])]
564 pub show_directories: bool,
565
566 #[arg(long, short = 'D')]
570 pub show_dot_files: bool,
571
572 #[arg(long)]
576 pub include_git_files: bool,
577
578 #[arg(add = ArgValueCompleter::new(xvc_path_completer))]
582 pub targets: Option<Vec<String>>,
583}
584
585impl UpdateFromXvcConfig for ListCLI {
586 fn update_from_conf(self, conf: &xvc_core::XvcConfig) -> XvcConfigResult<Box<Self>> {
587 let no_summary = self.no_summary || conf.get_bool("file.list.no_summary")?.option;
588 let show_dot_files =
589 self.show_dot_files || conf.get_bool("file.list.show_dot_files")?.option;
590
591 let format = self.format.unwrap_or_else(|| ListFormat::from_conf(conf));
592 let sort_criteria = self
593 .sort
594 .unwrap_or_else(|| ListSortCriteria::from_conf(conf));
595 let include_git_files =
596 self.include_git_files || conf.get_bool("file.list.include_git_files")?.option;
597
598 Ok(Box::new(Self {
599 no_summary,
600 show_dot_files,
601 include_git_files,
602 format: Some(format),
603 sort: Some(sort_criteria),
604 ..self
605 }))
606 }
607}
608
609pub fn cmd_list(output_snd: &XvcOutputSender, xvc_root: &XvcRoot, cli_opts: ListCLI) -> Result<()> {
630 let conf = xvc_root.config();
633 let opts = cli_opts.update_from_conf(conf)?;
634 let no_summary = opts.no_summary;
635 let list_rows = cmd_list_inner(output_snd, xvc_root, &opts)?;
636
637 output!(
640 output_snd,
641 "{}",
642 build_table(&list_rows, Box::new(build_row))
643 );
644 if !no_summary {
645 output!(output_snd, "{}", add_summary_line(&list_rows));
646 }
647
648 Ok(())
649}
650
651pub fn cmd_list_inner(
654 output_snd: &XvcOutputSender,
655 xvc_root: &XvcRoot,
656 opts: &ListCLI,
657) -> Result<ListRows> {
658 let conf = xvc_root.config();
659
660 let current_dir = conf.current_dir()?;
661 let filter_git_paths = !opts.include_git_files;
662
663 let all_from_disk = targets_from_disk(
664 output_snd,
665 xvc_root,
666 current_dir,
667 &opts.targets,
668 filter_git_paths,
669 )?;
670
671 let from_disk = filter_xvc_path_metadata_map(all_from_disk, opts);
672
673 let from_store = load_targets_from_store(output_snd, xvc_root, current_dir, &opts.targets)?;
674
675 let xvc_metadata_store = xvc_root.load_store::<XvcMetadata>()?;
676
677 let recheck_method_store = xvc_root.load_store::<RecheckMethod>()?;
678
679 let filter_keys = filter_xvc_path_xvc_metadata_stores(&from_store, &xvc_metadata_store, opts);
680
681 let from_store = from_store.subset(filter_keys.iter().copied())?;
682 let xvc_metadata_store = xvc_metadata_store.subset(filter_keys.iter().copied())?;
683 let recheck_method_store = recheck_method_store.subset(filter_keys.into_iter())?;
684
685 let matches = match_store_and_disk_paths(
686 from_disk,
687 from_store,
688 xvc_metadata_store,
689 recheck_method_store,
690 );
691
692 let matches = if opts.format.as_ref().unwrap().columns.iter().any(|c| {
693 *c == ListColumn::RecordedContentDigest64 || *c == ListColumn::RecordedContentDigest8
694 }) {
695 fill_recorded_content_digests(xvc_root, matches)?
696 } else {
697 matches
698 };
699
700 let matches =
701 if opts.format.as_ref().unwrap().columns.iter().any(|c| {
702 *c == ListColumn::ActualContentDigest64 || *c == ListColumn::ActualContentDigest8
703 }) {
704 let algorithm = HashAlgorithm::from_conf(conf);
705 fill_actual_content_digests(output_snd, xvc_root, algorithm, matches)?
706 } else {
707 matches
708 };
709
710 let path_prefix = current_dir.strip_prefix(xvc_root.absolute_path())?;
711
712 let rows = build_rows_from_matches(output_snd, matches, path_prefix);
713 let format = opts
714 .format
715 .clone()
716 .expect("Option must be filled at this point");
717 let sort_criteria = opts.sort.expect("Option must be filled at this point");
718
719 let list_rows = ListRows::new(format, sort_criteria, rows);
720 Ok(list_rows)
721}
722
723fn build_rows_from_matches(
724 output_snd: &XvcOutputSender,
725 matches: Vec<PathMatch>,
726 path_prefix: &Path,
727) -> Vec<ListRow> {
728 matches
729 .into_iter()
730 .filter_map(|pm| match ListRow::new(path_prefix, pm) {
731 Ok(lr) => Some(lr),
732 Err(e) => {
733 error!(output_snd, "{}", e);
734 None
735 }
736 })
737 .collect()
738}
739
740fn fill_actual_content_digests(
741 output_snd: &XvcOutputSender,
742 xvc_root: &XvcRoot,
743 algorithm: HashAlgorithm,
744 matches: Vec<PathMatch>,
745) -> Result<Vec<PathMatch>> {
746 let text_or_binary_store = xvc_root.load_store::<FileTextOrBinary>()?;
747 Ok(matches
748 .into_iter()
749 .filter_map(|pm| {
750 if pm
751 .actual_path
752 .as_deref()
753 .and(pm.actual_metadata.map(|md| md.is_file()))
754 == Some(true)
755 {
756 let actual_path = pm.actual_path.as_ref().unwrap();
757 let path = actual_path.to_absolute_path(xvc_root);
758 let text_or_binary = if let Some(xvc_entity) = pm.xvc_entity {
759 text_or_binary_store
760 .get(&xvc_entity)
761 .copied()
762 .unwrap_or_default()
763 } else {
764 FileTextOrBinary::default()
765 };
766
767 match ContentDigest::new(&path, algorithm, text_or_binary.as_inner()) {
768 Ok(digest) => Some(PathMatch {
769 actual_digest: Some(digest),
770 ..pm
771 }),
772 Err(e) => {
773 error!(output_snd, "{}", e);
774 None
775 }
776 }
777 } else {
778 Some(pm)
779 }
780 })
781 .collect())
782}
783
784fn fill_recorded_content_digests(
785 xvc_root: &std::sync::Arc<xvc_core::types::xvcroot::XvcRootInner>,
786 matches: Vec<PathMatch>,
787) -> Result<Vec<PathMatch>> {
788 let content_digest_store = xvc_root.load_store::<ContentDigest>()?;
789 let matches: Vec<PathMatch> = matches
790 .into_iter()
791 .map(|pm| {
792 if let Some(xvc_entity) = pm.xvc_entity {
793 let digest = content_digest_store.get(&xvc_entity).cloned();
794 PathMatch {
795 recorded_digest: digest,
796 ..pm
797 }
798 } else {
799 pm
800 }
801 })
802 .collect();
803 Ok(matches)
804}
805
806fn match_store_and_disk_paths(
812 from_disk: HashMap<XvcPath, XvcMetadata>,
813 from_store: HStore<XvcPath>,
814 stored_xvc_metadata: HStore<XvcMetadata>,
815 stored_recheck_method: HStore<RecheckMethod>,
816) -> Vec<PathMatch> {
817 let mut matches = Vec::<PathMatch>::new();
820
821 let mut found_entities = HashSet::<XvcEntity>::new();
822
823 for (disk_xvc_path, disk_xvc_md) in from_disk {
824 if let Some(xvc_entity) = from_store.entity_by_value(&disk_xvc_path) {
826 let recorded_metadata = stored_xvc_metadata.get(&xvc_entity).cloned();
827 let recorded_path = from_store.get(&xvc_entity).cloned();
828 let recorded_recheck_method = stored_recheck_method.get(&xvc_entity).cloned();
829 found_entities.insert(xvc_entity);
830 let pm = PathMatch {
831 xvc_entity: Some(xvc_entity),
832 actual_path: Some(disk_xvc_path),
833 actual_metadata: Some(disk_xvc_md),
834 actual_digest: None,
836
837 recorded_metadata,
838 recorded_path,
839 recorded_recheck_method,
840 recorded_digest: None,
841 };
842 matches.push(pm);
843 } else {
844 let pm = PathMatch {
846 xvc_entity: None,
847 actual_path: Some(disk_xvc_path),
848 actual_metadata: Some(disk_xvc_md),
849 actual_digest: None,
851
852 recorded_recheck_method: None,
853 recorded_metadata: None,
854 recorded_path: None,
855 recorded_digest: None,
856 };
857 matches.push(pm);
858 }
859 }
860
861 let not_found_entities = from_store
863 .keys()
864 .copied()
865 .filter(|xvc_entity| !found_entities.contains(xvc_entity))
866 .collect::<Vec<_>>();
867
868 for xvc_entity in ¬_found_entities {
869 let recorded_metadata = stored_xvc_metadata.get(xvc_entity).cloned();
870 let recorded_path = from_store.get(xvc_entity).cloned();
871 let recorded_recheck_method = stored_recheck_method.get(xvc_entity).cloned();
872 let pm = PathMatch {
873 xvc_entity: Some(*xvc_entity),
874 actual_path: None,
875 actual_metadata: None,
876 actual_digest: None,
878 recorded_path,
879 recorded_metadata,
880 recorded_recheck_method,
881 recorded_digest: None,
882 };
883 matches.push(pm);
884 }
885 matches
886}
887
888fn filter_xvc_path_metadata_map(
889 all_from_disk: HashMap<XvcPath, XvcMetadata>,
890 opts: &ListCLI,
891) -> HashMap<XvcPath, XvcMetadata> {
892 let filter_fn = match (opts.show_dot_files, opts.show_directories) {
893 (true, true) => |_, _| true,
894
895 (true, false) => |_, md: &XvcMetadata| !md.is_dir(),
896 (false, true) => |path: &XvcPath, _| !(path.starts_with_str(".") || path.contains("./")),
897 (false, false) => |path: &XvcPath, md: &XvcMetadata| {
898 !(path.starts_with_str(".") || path.contains("./") || md.is_dir())
899 },
900 };
901
902 all_from_disk
903 .iter()
904 .filter_map(|(path, md)| {
905 if filter_fn(path, md) {
906 Some((path.clone(), *md))
907 } else {
908 None
909 }
910 })
911 .collect()
912}
913
914fn filter_xvc_path_xvc_metadata_stores(
915 xvc_path_store: &HStore<XvcPath>,
916 xvc_metadata_store: &XvcStore<XvcMetadata>,
917 opts: &ListCLI,
918) -> HashSet<XvcEntity> {
919 let filter_fn = match (opts.show_dot_files, opts.show_directories) {
920 (true, true) => |_, _| true,
921
922 (true, false) => |_, md: &XvcMetadata| !md.is_dir(),
923 (false, true) => |path: &XvcPath, _| !(path.starts_with_str(".") || path.contains("./")),
924 (false, false) => |path: &XvcPath, md: &XvcMetadata| {
925 !(path.starts_with_str(".") || path.contains("./") || md.is_dir())
926 },
927 };
928
929 xvc_path_store
930 .iter()
931 .filter_map(|(xvc_entity, xvc_path)| {
932 if let Some(xvc_metadata) = xvc_metadata_store.get(xvc_entity) {
933 if filter_fn(xvc_path, xvc_metadata) {
934 Some(*xvc_entity)
935 } else {
936 None
937 }
938 } else {
939 None
940 }
941 })
942 .collect()
943}