Skip to main content

git_simple_encrypt/
cli.rs

1use std::path::{Path, PathBuf};
2
3use clap::{Parser, Subcommand};
4use config_file2::Storable;
5use log::{debug, info, warn};
6
7use crate::{
8    error::{Error, Result},
9    repo::Repo,
10};
11
12#[derive(Parser, Debug)]
13#[command(author, version, about, long_about = None, after_help = r#"Examples:
14git-se p                    # Set/update master password
15git-se add file.txt  mydir  # Add files/folders to the encryption list
16git-se e                    # Encrypt all files in the list
17git-se d                    # Decrypt all files in the list
18git-se e xxx.txt dir1 ...   # Encrypt specific files
19git-se d xxx.txt dir1 ...   # Decrypt specific files
20git-se i                    # Install a pre-commit hook to check encryption before committing
21"#)]
22#[clap(args_conflicts_with_subcommands = true)]
23pub struct Cli {
24    /// Encrypt, Decrypt and Add
25    #[command(subcommand)]
26    pub command: SubCommand,
27    /// Repository path, allow both relative and absolute path.
28    #[arg(short, long, global = true)]
29    #[clap(value_parser = repo_path_parser, default_value = ".")]
30    pub repo: PathBuf,
31}
32
33fn repo_path_parser(path: &str) -> Result<PathBuf, String> {
34    match path_absolutize::Absolutize::absolutize(Path::new(path)) {
35        Ok(p) => Ok(p.into_owned()),
36        Err(e) => Err(e.to_string()),
37    }
38}
39
40#[derive(Subcommand, Debug)]
41pub enum SubCommand {
42    /// Encrypt all files with crypt attr.
43    #[clap(alias("e"))]
44    Encrypt {
45        /// The files or folders to be encrypted.
46        paths: Vec<PathBuf>,
47    },
48    /// Decrypt all files with crypt attr and `.enc` extension.
49    #[clap(alias("d"))]
50    Decrypt {
51        /// The files or folders to be decrypted.
52        paths: Vec<PathBuf>,
53    },
54    /// Mark files or folders as need-to-be-crypted.
55    Add { paths: Vec<PathBuf> },
56    /// Set key or other config items.
57    Set {
58        #[clap(subcommand)]
59        field: SetField,
60    },
61    /// Set password interactively.
62    #[clap(alias("p"))]
63    Pwd,
64    /// Check if all files in the crypt list are encrypted.
65    #[clap(alias("c"))]
66    Check {
67        /// The files or folders to check. If empty, checks all files in the
68        /// crypt list.
69        paths: Vec<PathBuf>,
70        /// Only check files staged for commit (used by pre-commit hook).
71        #[arg(long, default_value_t = false)]
72        staged: bool,
73    },
74    /// Install a pre-commit hook to check encryption before committing.
75    #[clap(alias("i"))]
76    Install,
77}
78
79#[derive(Debug, Subcommand)]
80pub enum SetField {
81    /// Set key
82    Key { value: String },
83    /// Set zstd compression level
84    ZstdLevel {
85        #[clap(value_parser = validate_zstd_level)]
86        value: u8,
87    },
88    /// Set zstd compression enable or not
89    EnableZstd {
90        #[clap(value_parser = validate_bool)]
91        value: bool,
92    },
93}
94
95impl SetField {
96    /// Apply the field update to the given repo's config.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the underlying git command or the config file write
101    /// fails.
102    pub fn set(&self, repo: &mut Repo) -> Result<()> {
103        match self {
104            Self::Key { value } => {
105                warn!("`set key` is deprecated, please use `pwd` or `p` instead.");
106                repo.set_config("key", value)?;
107                info!("Master key updated.");
108            }
109            Self::EnableZstd { value } => {
110                repo.conf.use_zstd = *value;
111                info!("zstd compression enabled: {value}");
112            }
113            Self::ZstdLevel { value } => {
114                repo.conf.zstd_level = *value;
115                info!("zstd compression level set to {value}");
116            }
117        }
118        debug!("store config to {}", repo.conf.config_path.display());
119        repo.conf
120            .store()
121            .map_err(|e| Error::Config(e.to_string()))?;
122        Ok(())
123    }
124}
125
126fn validate_zstd_level(value: &str) -> Result<u8, String> {
127    let value = value
128        .parse::<u8>()
129        .map_err(|_| "value should be a number")?;
130    if (1..=22_u8).contains(&value) {
131        Ok(value)
132    } else {
133        Err("value should be 1-22".to_string())
134    }
135}
136
137fn validate_bool(value: &str) -> Result<bool, String> {
138    match value {
139        "true" | "1" => Ok(true),
140        "false" | "0" => Ok(false),
141        _ => Err("value should be `true`, `false`, `1` or `0`".into()),
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use assert2::assert;
148
149    use super::*;
150
151    #[test]
152    fn repo_path_parser_resolves_relative() {
153        // "." should absolutize to the current working directory.
154        let parsed = repo_path_parser(".").unwrap();
155        assert!(parsed.is_absolute());
156    }
157}