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