1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
use crate::cli::Cli;
use std::io::{self, Write};
use std::process::{Command, Output};

// Convenient Key, Value structures to be used with a HashMap.
// Key is identified by the typo and its suggested corrections
#[derive(Eq, Hash, PartialEq, Debug)]
pub struct Key {
    pub typo: String,
    pub corrections: Vec<String>,
}

impl Key {
    // Returns true if the Key contains a typo that may be corrected
    pub fn is_typo_correctable(&self, cli: &Cli) -> bool {
        self.corrections.len() == 1 && self.typo.len() >= cli.minlen
    }

    // Runs sed command. This is way too basic and may lead to replacement
    // mistakes.
    pub fn run_sed(&self, files: &Vec<String>, cli: &Cli) {
        if self.is_typo_correctable(cli) {
            // Here we know that we have exactly one correction
            let sed_script = format!("s/\\b{}\\b/{}/g", self.typo, self.corrections[0]);

            if cli.noop {
                println!("sed --in-place --expression={sed_script} {files:?}");
            } else {
                let output = Command::new("sed").arg("--in-place").arg(format!("--expression={sed_script}")).args(files).output().expect("failed to execute sed process");

                if !output.status.success() {
                    output_detected_errors(output, &format!("Error while executing sed ({} -> {:?})", self.typo, self.corrections[0]));
                }
            }
        }
    }

    // Runs git commit command.
    // @TODO: check whether the sed command did modify something before
    //        trying to commit anything
    // @TODO: avoid spawning external process
    pub fn run_git_commit(&self, cli: &Cli) {
        if self.is_typo_correctable(cli) {
            // Here we know that we have exactly one correction
            let replaced = cli.message.replace("{typo}", &self.typo);
            let typo_message = match cli.message.contains("{typo}") {
                true => &replaced,
                false => &cli.message,
            };

            let replaced = typo_message.replace("{correction}", &self.corrections[0]);
            let git_message = match cli.message.contains("{correction}") {
                true => &replaced,
                false => typo_message,
            };

            if cli.noop {
                println!("git commit --all --message={git_message}");
            } else {
                let output = Command::new("git").arg("commit").arg("--all").arg(format!("--message={git_message}")).output().expect("failed to execute git process");

                if !output.status.success() {
                    output_detected_errors(output, &format!("Error while executing git commit ({} -> {:?})", self.typo, self.corrections[0]));
                }
            }
        }
    }
}

// Value contains information about where the typo is located
#[derive(Eq, Hash, PartialEq, Debug)]
pub struct Value {
    pub path: String,
    pub line_num: u32,
    pub byte_offset: u32,
}

// Helper function to print the output of the command that
// has been executed and that returned an error status.
// Prints an another message if an error occurred when printing
// to stderr or stdout.
fn output_detected_errors(output: Output, message: &str) {
    eprintln!("{message}");
    if let Err(e) = io::stderr().write_all(&output.stderr) {
        eprintln!("Error while writing to stderr: {e}");
    }
    if let Err(e) = io::stdout().write_all(&output.stdout) {
        eprintln!("Error while writing to stdout: {e}");
    }
}