1use crate::error::{Error, Result};
6use clap::Parser;
7use crossbeam_channel::unbounded;
8use log::warn;
9use std::{env, path::PathBuf};
10use xvc_core::AbsolutePath;
11use xvc_core::ContentDigest;
12use xvc_core::FromConfig;
13use xvc_core::UpdateFromConfig;
14use xvc_core::XvcConfig;
15use xvc_core::XvcConfigResult;
16use xvc_core::XvcConfiguration;
17use xvc_core::XvcLoadParams;
18use xvc_core::{output, XvcOutputSender};
19use xvc_core::{
20 util::file::{path_metadata_channel, pipe_filter_path_errors},
21 HashAlgorithm, TextOrBinary, XvcRoot,
22};
23
24use crate::common::pipe_path_digest;
25
26#[derive(Debug, Clone, PartialEq, Eq, Parser)]
27#[command(version, author)]
28pub struct HashCLI {
34 #[arg(short, long)]
37 algorithm: Option<HashAlgorithm>,
38 #[arg(long, default_value("auto"))]
42 text_or_binary: TextOrBinary,
43
44 #[arg()]
48 targets: Vec<PathBuf>,
49}
50
51impl UpdateFromConfig for HashCLI {
52 fn update_from_config(self, conf: &XvcConfiguration) -> XvcConfigResult<Box<Self>> {
53 let algorithm = self.algorithm.unwrap_or_else(|| {
54 *HashAlgorithm::from_config(conf).expect("HashAlgorithm must be configured")
55 });
56
57 Ok(Box::new(Self {
58 algorithm: Some(algorithm),
59 text_or_binary: self.text_or_binary,
60 targets: self.targets.clone(),
61 }))
62 }
63}
64pub fn cmd_hash(
68 output_snd: &XvcOutputSender,
69 xvc_root: Option<&XvcRoot>,
70 opts: HashCLI,
71) -> Result<()> {
72 let conf = match xvc_root {
73 Some(xvc_root) => xvc_root.config().clone(),
74 None => XvcConfig::new_v2(&XvcLoadParams {
75 current_dir: AbsolutePath::from(env::current_dir()?),
76 xvc_root_dir: None,
77 include_system_config: true,
78 include_user_config: true,
79 include_project_config: false,
80 include_local_config: false,
81 project_config_path: None,
82 local_config_path: None,
83 include_environment_config: true,
84 command_line_config: None,
85 })?
86 .config()
87 .clone(),
88 };
89
90 let opts = opts.update_from_config(&conf)?;
91 let algorithm = opts.algorithm.unwrap_or(HashAlgorithm::Blake3);
92
93 let text_or_binary = opts.text_or_binary;
94 let targets = opts.targets;
95
96 for t in targets {
97 if !t.exists() {
98 Error::FileNotFound { path: t }.error();
99 continue;
100 }
101 if t.is_dir() {
102 let (path_snd, path_rec) = unbounded();
103 path_metadata_channel(path_snd, &t)?;
104 let (filtered_path_snd, filtered_path_rec) = unbounded();
105 pipe_filter_path_errors(path_rec, filtered_path_snd)?;
106 let (digest_snd, digest_rec) = unbounded();
107 pipe_path_digest(filtered_path_rec, digest_snd, algorithm, text_or_binary)?;
108
109 for (path, digest) in digest_rec {
110 output!(output_snd, "{digest}\t{}", path.to_string_lossy());
111 }
112 } else if t.is_file() {
113 let digest = ContentDigest::new(&t, algorithm, text_or_binary)?;
114 output!(output_snd, "{digest}\t{}", t.to_string_lossy());
115 } else {
116 warn!("Unsupported FS Type: {:?}", t);
117 }
118 }
119
120 Ok(())
121}