Skip to main content

typos_git_commit/
keyvalue.rs

1use crate::cli::Cli;
2use fluent_i18n::t;
3use std::io::{self, Write};
4use std::process::{Command, Output};
5
6// Convenient Key, Value structures to be used with a HashMap.
7// Key is identified by the typo and its suggested corrections
8#[derive(Eq, Hash, PartialEq, Debug)]
9pub struct Key {
10    pub typo: String,
11    pub corrections: Vec<String>,
12}
13
14// Helper function to print the output of the command that
15// has been executed and that returned an error status.
16// Prints an another message if an error occurred when printing
17// to stderr or stdout.
18fn output_detected_errors(output: &Output, message: &str) {
19    eprintln!("{message}");
20    if let Err(e) = io::stderr().write_all(&output.stderr) {
21        eprintln!("{}", t!("keyvalue-error-stderr", {"e" => e.to_string()}));
22    }
23    if let Err(e) = io::stdout().write_all(&output.stdout) {
24        eprintln!("{}", t!("keyvalue-error-stdout", {"e" => e.to_string()}));
25    }
26}
27
28impl Key {
29    /// Returns true if the Key contains a typo that may be corrected
30    #[must_use]
31    pub fn is_typo_correctable(&self, cli: &Cli) -> bool {
32        match &cli.typo {
33            Some(typos) => typos.contains(&self.typo) && self.corrections.len() == 1 && self.typo.len() >= cli.minlen,
34            None => self.corrections.len() == 1 && self.typo.len() >= cli.minlen,
35        }
36    }
37
38    /// Runs sed command. This is way too basic and may lead to replacement
39    /// mistakes.
40    ///
41    /// # Panics
42    ///
43    /// Panics if the sed subprocess fails
44    pub fn run_sed(&self, files: &Vec<String>, cli: &Cli) {
45        if !self.is_typo_correctable(cli) {
46            return;
47        }
48
49        // Here we know that we have exactly one correction
50        let sed_script = format!("s/\\b{}\\b/{}/g", self.typo, self.corrections[0]);
51
52        if cli.noop {
53            println!("sed --in-place --expression={sed_script} {files:?}");
54        } else {
55            let output = Command::new("sed")
56                .arg("--in-place")
57                .arg(format!("--expression={sed_script}"))
58                .args(files)
59                .output()
60                .unwrap_or_else(|_| panic!("{}", t!("keyvalue-process-sed")));
61
62            if !output.status.success() {
63                output_detected_errors(&output, t!("keyvalue-error-sed", { "typo" => self.typo, "correction" => format!("{:?}", self.corrections[0])}).as_str());
64            }
65        }
66    }
67
68    /// Replaces {typo} and {correction} by their respective values
69    /// self.corrections is expected to have one (and exactly one) value
70    fn format_git_message(&self, cli: &Cli) -> String {
71        if let Some(correction) = self.corrections.first() {
72            cli.message.replace("{typo}", &self.typo).replace("{correction}", correction)
73        } else {
74            cli.message.replace("{typo}", &self.typo)
75        }
76    }
77
78    /// Runs git commit command.
79    ///
80    /// # Panics
81    ///
82    /// Panics if the git subprocess fails
83    // @TODO: check whether the sed command did modify something before
84    //        trying to commit anything
85    // @TODO: avoid spawning external process
86    pub fn run_git_commit(&self, cli: &Cli) {
87        if !self.is_typo_correctable(cli) {
88            return;
89        }
90
91        // Here we know that we have exactly one correction (because it
92        // is a correctable typo)
93        let git_message = self.format_git_message(cli);
94
95        if cli.noop {
96            println!("git commit --all --message={git_message}");
97        } else {
98            // @todo: detect if a correction has been done or not as we do
99            // only correct whole words. This is to avoid a blank commit and
100            // get an error. When detected print a message about the typo that
101            // was not corrected
102            let output = Command::new("git")
103                .arg("commit")
104                .arg("--all")
105                .arg(format!("--message={git_message}"))
106                .output()
107                .expect("failed to execute git process");
108
109            if !output.status.success() {
110                output_detected_errors(&output, t!("keyvalue-error-git", { "typo" => self.typo, "correction" => format!("{:?}", self.corrections[0])}).as_str());
111            }
112        }
113    }
114}
115
116// Value contains information about where the typo is located
117#[derive(Eq, Hash, PartialEq, Debug)]
118pub struct Value {
119    pub path: String,
120    pub line_num: u32,
121    pub byte_offset: u32,
122}
123
124impl Value {
125    pub fn print_value_details(&self) {
126        println!(
127            "\t{}",
128            t!("thashmap-typo-details", {"path" => self.path, "line" => self.line_num, "offset" => self.byte_offset})
129        );
130    }
131}