diff --git a/src/cli/mod.rs b/src/cli/mod.rs index e69de29..8b13789 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -0,0 +1 @@ + diff --git a/src/commit/mod.rs b/src/commit/mod.rs index e69de29..cd40856 100644 --- a/src/commit/mod.rs +++ b/src/commit/mod.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/src/commit/types.rs b/src/commit/types.rs new file mode 100644 index 0000000..85c33a5 --- /dev/null +++ b/src/commit/types.rs @@ -0,0 +1,277 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CommitType { + Feat, + Fix, + Docs, + Style, + Refactor, + Perf, + Test, + Build, + Ci, + Chore, + Revert, +} + +impl CommitType { + pub fn all() -> &'static [Self] { + &[ + Self::Feat, + Self::Fix, + Self::Docs, + Self::Style, + Self::Refactor, + Self::Perf, + Self::Test, + Self::Build, + Self::Ci, + Self::Chore, + Self::Revert, + ] + } + + pub const fn description(&self) -> &'static str { + match self { + Self::Feat => "A new feature", + Self::Fix => "A bug fix", + Self::Docs => "Documentation only changes", + Self::Style => "Changes that do not affect the meaning of the code", + Self::Refactor => "A code change that neither fixes a bug nor adds a feature", + Self::Perf => "A code change that improves performance", + Self::Test => "Adding missing tests or correcting existing tests", + Self::Build => "Changes that affect the build system or external dependencies", + Self::Ci => "Changes to CI configuration files and scripts", + Self::Chore => "Other changes that don't modify src or test files", + Self::Revert => "Reverts a previous commit", + } + } + + pub const fn as_str(&self) -> &'static str { + match self { + Self::Feat => "feat", + Self::Fix => "fix", + Self::Docs => "docs", + Self::Style => "style", + Self::Refactor => "refactor", + Self::Perf => "perf", + Self::Test => "test", + Self::Build => "build", + Self::Ci => "ci", + Self::Chore => "chore", + Self::Revert => "revert", + } + } +} + +impl std::fmt::Display for CommitType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that all 11 commit types exist and can be constructed + #[test] + fn all_eleven_variants_exist() { + // Exhaustive pattern matching ensures all variants exist at compile time + let variants = [ + CommitType::Feat, + CommitType::Fix, + CommitType::Docs, + CommitType::Style, + CommitType::Refactor, + CommitType::Perf, + CommitType::Test, + CommitType::Build, + CommitType::Ci, + CommitType::Chore, + CommitType::Revert, + ]; + assert_eq!(variants.len(), 11); + } + + /// Test that as_str() returns the correct lowercase string for each variant + #[test] + fn as_str_returns_lowercase_string() { + assert_eq!(CommitType::Feat.as_str(), "feat"); + assert_eq!(CommitType::Fix.as_str(), "fix"); + assert_eq!(CommitType::Docs.as_str(), "docs"); + assert_eq!(CommitType::Style.as_str(), "style"); + assert_eq!(CommitType::Refactor.as_str(), "refactor"); + assert_eq!(CommitType::Perf.as_str(), "perf"); + assert_eq!(CommitType::Test.as_str(), "test"); + assert_eq!(CommitType::Build.as_str(), "build"); + assert_eq!(CommitType::Ci.as_str(), "ci"); + assert_eq!(CommitType::Chore.as_str(), "chore"); + assert_eq!(CommitType::Revert.as_str(), "revert"); + } + + /// Test that as_str() output is always lowercase (property-based check) + #[test] + fn as_str_is_always_lowercase() { + for commit_type in CommitType::all() { + let s = commit_type.as_str(); + assert_eq!( + s, + s.to_lowercase(), + "as_str() should return lowercase for {:?}", + commit_type + ); + } + } + + /// Test that description() returns a non-empty string for each variant + #[test] + fn description_returns_non_empty_string() { + for commit_type in CommitType::all() { + let desc = commit_type.description(); + assert!( + !desc.is_empty(), + "description() should not be empty for {:?}", + commit_type + ); + } + } + + /// Test that description() returns the expected descriptions per spec + #[test] + fn description_returns_expected_values() { + assert_eq!(CommitType::Feat.description(), "A new feature"); + assert_eq!(CommitType::Fix.description(), "A bug fix"); + assert_eq!(CommitType::Docs.description(), "Documentation only changes"); + assert_eq!( + CommitType::Style.description(), + "Changes that do not affect the meaning of the code" + ); + assert_eq!( + CommitType::Refactor.description(), + "A code change that neither fixes a bug nor adds a feature" + ); + assert_eq!( + CommitType::Perf.description(), + "A code change that improves performance" + ); + assert_eq!( + CommitType::Test.description(), + "Adding missing tests or correcting existing tests" + ); + assert_eq!( + CommitType::Build.description(), + "Changes that affect the build system or external dependencies" + ); + assert_eq!( + CommitType::Ci.description(), + "Changes to CI configuration files and scripts" + ); + assert_eq!( + CommitType::Chore.description(), + "Other changes that don't modify src or test files" + ); + assert_eq!( + CommitType::Revert.description(), + "Reverts a previous commit" + ); + } + + /// Test that all() returns exactly 11 types + #[test] + fn all_returns_eleven_types() { + assert_eq!(CommitType::all().len(), 11); + } + + /// Test that all() returns types in the expected order (feat first, revert last) + #[test] + fn all_returns_types_in_expected_order() { + let all = CommitType::all(); + assert_eq!(all[0], CommitType::Feat); + assert_eq!(all[1], CommitType::Fix); + assert_eq!(all[2], CommitType::Docs); + assert_eq!(all[3], CommitType::Style); + assert_eq!(all[4], CommitType::Refactor); + assert_eq!(all[5], CommitType::Perf); + assert_eq!(all[6], CommitType::Test); + assert_eq!(all[7], CommitType::Build); + assert_eq!(all[8], CommitType::Ci); + assert_eq!(all[9], CommitType::Chore); + assert_eq!(all[10], CommitType::Revert); + } + + /// Test that all() contains all unique variants (no duplicates) + #[test] + fn all_contains_unique_variants() { + let all = CommitType::all(); + for (i, variant) in all.iter().enumerate() { + for (j, other) in all.iter().enumerate() { + if i != j { + assert_ne!(variant, other, "all() should not contain duplicates"); + } + } + } + } + + /// Test that Display implementation delegates to as_str() + #[test] + fn display_delegates_to_as_str() { + for commit_type in CommitType::all() { + let display_output = format!("{}", commit_type); + let as_str_output = commit_type.as_str(); + assert_eq!( + display_output, as_str_output, + "Display should delegate to as_str() for {:?}", + commit_type + ); + } + } + + /// Test Display for specific variants + #[test] + fn display_shows_lowercase_type() { + assert_eq!(format!("{}", CommitType::Feat), "feat"); + assert_eq!(format!("{}", CommitType::Fix), "fix"); + assert_eq!(format!("{}", CommitType::Docs), "docs"); + assert_eq!(format!("{}", CommitType::Style), "style"); + assert_eq!(format!("{}", CommitType::Refactor), "refactor"); + assert_eq!(format!("{}", CommitType::Perf), "perf"); + assert_eq!(format!("{}", CommitType::Test), "test"); + assert_eq!(format!("{}", CommitType::Build), "build"); + assert_eq!(format!("{}", CommitType::Ci), "ci"); + assert_eq!(format!("{}", CommitType::Chore), "chore"); + assert_eq!(format!("{}", CommitType::Revert), "revert"); + } + + /// Test that CommitType implements Copy (can be used after move) + #[test] + fn commit_type_is_copy() { + let original = CommitType::Feat; + let copied = original; // Copy, not move + assert_eq!(original, copied); // original still usable + } + + /// Test that CommitType implements PartialEq correctly + #[test] + fn commit_type_equality() { + assert_eq!(CommitType::Feat, CommitType::Feat); + assert_ne!(CommitType::Feat, CommitType::Fix); + } + + /// Test that CommitType can be used as HashMap key (Hash + Eq) + #[test] + fn commit_type_can_be_hash_key() { + use std::collections::HashMap; + let mut map = HashMap::new(); + map.insert(CommitType::Feat, "feature"); + map.insert(CommitType::Fix, "bugfix"); + assert_eq!(map.get(&CommitType::Feat), Some(&"feature")); + assert_eq!(map.get(&CommitType::Fix), Some(&"bugfix")); + } + + /// Test that Debug is implemented + #[test] + fn commit_type_has_debug() { + let debug_output = format!("{:?}", CommitType::Feat); + assert!(debug_output.contains("Feat")); + } +} diff --git a/src/error.rs b/src/error.rs index 48a5f40..32910d9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,5 +16,5 @@ pub enum Error { #[error("Operation cancelled by user")] Cancelled, #[error("Non-interactive terminal detected")] - NonInteractive + NonInteractive, } diff --git a/src/jj/mod.rs b/src/jj/mod.rs index e69de29..8b13789 100644 --- a/src/jj/mod.rs +++ b/src/jj/mod.rs @@ -0,0 +1 @@ + diff --git a/src/lib.rs b/src/lib.rs index 1b4308e..36bdc97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ mod cli; mod commit; +mod error; mod jj; mod prompts; -mod error; diff --git a/src/prompts/mod.rs b/src/prompts/mod.rs index e69de29..8b13789 100644 --- a/src/prompts/mod.rs +++ b/src/prompts/mod.rs @@ -0,0 +1 @@ +