diff --git a/Cargo.lock b/Cargo.lock index a239993..71420ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ansi-escapes" @@ -40,6 +40,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "cc" version = "1.0.73" @@ -63,7 +69,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim", "textwrap", "unicode-width", @@ -86,6 +92,7 @@ dependencies = [ "git2", "log-update", "structopt", + "tempfile", "termion", ] @@ -95,6 +102,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e27ac934441d69b298c02aca5ee87a048b3d220d715557446ea12c61ccf5033" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -110,7 +133,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libgit2-sys", "log", @@ -164,9 +187,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libgit2-sys" @@ -182,6 +205,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] + [[package]] name = "libssh2-sys" version = "0.2.23" @@ -208,6 +242,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" version = "0.4.17" @@ -228,9 +268,15 @@ dependencies = [ [[package]] name = "numtoa" -version = "0.1.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl-probe" @@ -307,20 +353,30 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] name = "redox_termios" -version = "0.1.2" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + +[[package]] +name = "rustix" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "redox_syscall", + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -364,15 +420,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "termion" -version = "1.5.6" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +checksum = "7eaa98560e51a2cf4f0bb884d8b2098a9ea11ecf3b7078e9c68242c74cc923a7" dependencies = [ "libc", + "libredox", "numtoa", - "redox_syscall", "redox_termios", ] @@ -483,3 +552,76 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 6350cfe..0fd6aec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ readme = "readme.md" license = "MIT" [dependencies] -termion = "1.0" +termion = "4" log-update = "~0.1.0" default-editor = "~0.1.0" emoji-commit-type = "~0.1.1" @@ -21,6 +21,9 @@ git2 = "~0.15.0" structopt = "~0.3" ansi_term = "~0.12" +[dev-dependencies] +tempfile = "~3.14.0" + [profile.release] codegen-units = 1 lto = true diff --git a/src/main.rs b/src/main.rs index e6fce13..a03801f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::env; use std::error::Error; use std::fmt; use std::fs::File; -use std::io::{BufRead, BufReader, Seek, Write, stderr, stdin}; +use std::io::{Seek, Write, stderr, stdin}; use std::process::{Command, exit}; use std::path::PathBuf; use std::str::FromStr; @@ -12,12 +12,14 @@ use termion::input::TermRead; use termion::raw::IntoRawMode; use emoji_commit_type::CommitType; +use message::{git_parse_existing_message, git_message_is_empty}; use log_update::LogUpdate; use structopt::StructOpt; use ansi_term::Colour::{RGB, Green, Red, White}; mod commit_rules; mod git; +mod message; impl fmt::Display for commit_rules::CommitRuleValidationResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -42,14 +44,14 @@ fn commit_type_at_index (index: u8) -> Option { CommitType::iter_variants().nth(index as usize) } -fn select_emoji() -> Option<&'static str> { +fn select_emoji(initial_selected: Option) -> Option<&'static str> { let mut log_update = LogUpdate::new(stderr()).unwrap(); let mut raw_output = stderr().into_raw_mode().unwrap(); let mut key_stream = stdin().keys(); let mut aborted = false; - let mut selected = CommitType::Breaking; + let mut selected = initial_selected.unwrap_or(CommitType::Breaking); // Clear possibly printed "hint" from git log_update.render("").unwrap(); @@ -73,14 +75,14 @@ fn select_emoji() -> Option<&'static str> { if aborted { None } else { Some(selected.emoji()) } } -fn collect_commit_message(selected_emoji: &'static str, launch_editor: &mut bool) -> Option { +fn collect_commit_message(selected_emoji: &'static str, initial_message: Option, launch_editor: &mut bool) -> Option { let mut log_update = LogUpdate::new(stderr()).unwrap(); let mut raw_output = stderr().into_raw_mode().unwrap(); let mut key_stream = stdin().keys(); let mut aborted = false; - let mut input = String::new(); + let mut input = initial_message.unwrap_or(String::new()); loop { let rule_text = commit_rules::check_message(&input) @@ -143,33 +145,30 @@ fn launch_git_with_self_as_editor() { run_cmd(Command::new("git").arg("commit").env("GIT_EDITOR", self_path)) } -fn git_message_is_empty(file: &mut File) -> bool { - for line in BufReader::new(file).lines() { - let line = line.expect("Failed to read line from git message file"); - - if !line.starts_with('#') && !line.is_empty() { - return false; - } - } - true -} - fn collect_information_and_write_to_file(out_path: PathBuf) { let mut file = File::options().read(true).write(true).create(true).open(&out_path).unwrap(); + let mut initial_message: Option = None; + let mut initial_commit_type: Option = None; if !git_message_is_empty(&mut file) { - launch_default_editor(out_path); - return; + file.rewind().unwrap(); + if let Some((commit_type, message)) = git_parse_existing_message(&mut file) { + initial_commit_type = Some(commit_type); + initial_message = Some(message); + } else { + launch_default_editor(out_path); + return; + } } - let maybe_emoji = select_emoji(); + let maybe_emoji = select_emoji(initial_commit_type); if maybe_emoji == None { abort(); } if let Some(emoji) = maybe_emoji { let mut launch_editor = false; - let maybe_message = collect_commit_message(emoji, &mut launch_editor); + let maybe_message = collect_commit_message(emoji, initial_message, &mut launch_editor); if maybe_message == None { abort(); } diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000..35bd740 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,90 @@ +use emoji_commit_type::CommitType; +use std::{ + fs::File, + io::{BufRead, BufReader, Read}, +}; +use termion::input::TermRead; + +pub fn git_parse_existing_message(reader: &mut R) -> Option<(CommitType, String)> { + let first_line = reader.read_line().unwrap().unwrap(); + + if first_line.is_empty() { + return None; + } + + let first_str = first_line.chars().next().unwrap().to_string(); + + let commit_type = + CommitType::iter_variants().find(|commit_type| first_str == commit_type.emoji()); + + if commit_type == None { + return None; + } + + // Check that the rest of the commit message is empty (i.e. no body) + if !git_message_is_empty(reader) { + return None; + } + + let emoji = commit_type.unwrap().emoji().to_string(); + let message = first_line.replace(&emoji, "").trim().to_string(); + Some((commit_type.unwrap(), message)) +} + +pub(crate) fn git_message_is_empty(reader: R) -> bool { + for line in BufReader::new(reader).lines() { + let line = line.expect("Failed to read line from git message file"); + + if !line.starts_with('#') && !line.is_empty() { + return false; + } + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Seek, Write}; + use tempfile::NamedTempFile; + + #[test] + fn test_git_message_is_empty() { + let test_cases: Vec<(&str, bool)> = vec![ + ("# Comment line\n\n# Another comment", true), + ("yo", false), + ("# Comment\nActual content", false), + ("", true), + ]; + + for (input, expected) in test_cases { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "{}", input).unwrap(); + file.flush().unwrap(); + file.rewind().unwrap(); + assert_eq!(git_message_is_empty(&mut file), expected); + } + } + + #[test] + fn test_git_parse_existing_message() { + let test_cases: Vec<(&str, Option<(CommitType, &str)>)> = vec![ + ("🎉 Added a new feature", Some((CommitType::Feature, "Added a new feature"))), + ("Invalid message", None), + ("🐛 Fix critical bug", Some((CommitType::Bugfix, "Fix critical bug"))), + ("🎉 Added a new feature\n\n# Comment", Some((CommitType::Feature, "Added a new feature"))), + ("💥", Some((CommitType::Breaking, ""))), + ]; + + for (input, expected) in test_cases { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "{}", input).unwrap(); + file.flush().unwrap(); + file.rewind().unwrap(); + assert_eq!( + git_parse_existing_message(&mut file), + expected.map(|(t, m)| (t, m.to_string())) + ); + } + } +}