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