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