xvc_file/hash/
mod.rs

1//! Data structures and functions for `xvc file hash`.
2//!
3//! - [HashCLI] defines the command line options.
4//! - [cmd_hash] is the entry point for the command.
5use 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)]
28/// Calculate hash of given files
29///
30/// Note that this doesn't use .xvcignore facility and doesn't require an xvc root. It loads the
31/// configuration from xvc repository if it runs within, otherwise uses user, system or default
32/// options.
33pub struct HashCLI {
34    /// Algorithm to calculate the hash. One of blake3, blake2, sha2, sha3. All algorithm variants produce
35    /// 32-bytes digest.
36    #[arg(short, long)]
37    algorithm: Option<HashAlgorithm>,
38    /// For "text" remove line endings before calculating the digest. Keep line endings if
39    /// "binary". "auto" (default) detects the type by checking 0s in the first 8Kbytes, similar to
40    /// Git.
41    #[arg(long, default_value("auto"))]
42    text_or_binary: TextOrBinary,
43
44    /// Files to process
45    ///
46    /// NOTE: This uses the default completion as the command can work anywhere with any file
47    #[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}
64/// Entry point for `xvc file hash`.
65///
66/// Calculate hash of given files in `opts.targets` and send to `output_snd`.
67pub 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}