#![deny(unsafe_code)]
use crate::git_helpers::runtime_identity::{get_system_hostname, get_system_username};
use crate::ProcessExecutor;
#[cfg(test)]
use crate::executor::RealProcessExecutor;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdentityValidationError {
EmptyName,
EmptyEmail,
InvalidEmailFormat(String),
}
impl std::fmt::Display for IdentityValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyName => write!(f, "Git user name cannot be empty"),
Self::EmptyEmail => write!(f, "Git user email cannot be empty"),
Self::InvalidEmailFormat(email) => {
write!(f, "Invalid email format: '{email}'")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GitIdentity {
pub name: String,
pub email: String,
}
impl GitIdentity {
#[must_use]
pub const fn new(name: String, email: String) -> Self {
Self { name, email }
}
pub fn validate(&self) -> Result<(), IdentityValidationError> {
validate_git_identity_fields(&self.name, &self.email)
}
}
pub fn validate_git_identity_fields(
name: &str,
email: &str,
) -> Result<(), IdentityValidationError> {
if name.trim().is_empty() {
return Err(IdentityValidationError::EmptyName);
}
if email.trim().is_empty() {
return Err(IdentityValidationError::EmptyEmail);
}
let email = email.trim();
if !email.contains('@') {
return Err(IdentityValidationError::InvalidEmailFormat(
email.to_string(),
));
}
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return Err(IdentityValidationError::InvalidEmailFormat(
email.to_string(),
));
}
if parts[0].trim().is_empty() {
return Err(IdentityValidationError::InvalidEmailFormat(
email.to_string(),
));
}
if parts[1].trim().is_empty() || !parts[1].contains('.') {
return Err(IdentityValidationError::InvalidEmailFormat(
email.to_string(),
));
}
Ok(())
}
pub fn choose_username(env_username: Option<String>, whoami_output: Option<String>) -> String {
env_username
.filter(|u| !u.is_empty())
.or_else(|| whoami_output.map(|o| o.trim().to_string()))
.filter(|u| !u.is_empty())
.unwrap_or_else(|| "Unknown User".to_string())
}
pub fn choose_hostname(
env_hostname: Option<String>,
hostname_output: Option<String>,
) -> Option<String> {
env_hostname
.filter(|h| !h.is_empty())
.or_else(|| hostname_output.map(|h| h.trim().to_string()))
.filter(|h| !h.is_empty())
}
#[must_use]
pub fn fallback_username(executor: Option<&dyn ProcessExecutor>) -> String {
let env_username = get_system_username();
let whoami_output = if cfg!(unix) {
executor.and_then(|exec| {
exec.execute("whoami", &[], &[], None)
.ok()
.map(|o| o.stdout)
})
} else {
None
};
choose_username(env_username, whoami_output)
}
#[must_use]
pub fn fallback_email(username: &str, executor: Option<&dyn ProcessExecutor>) -> String {
let hostname = resolve_hostname_impl(executor);
let host = hostname.unwrap_or_else(|| "localhost".to_string());
format!("{username}@{host}")
}
fn resolve_hostname_impl(executor: Option<&dyn ProcessExecutor>) -> Option<String> {
let env_hostname = get_system_hostname();
let hostname_output = executor.and_then(|exec| {
exec.execute("hostname", &[], &[], None)
.ok()
.map(|o| o.stdout.trim().to_string())
});
choose_hostname(env_hostname, hostname_output)
}
#[must_use]
pub fn default_identity() -> GitIdentity {
GitIdentity::new("Ralph Workflow".to_string(), "ralph@localhost".to_string())
}
#[cfg(test)]
trait ContainsErr {
fn contains_err(&self, needle: &str) -> bool;
}
#[cfg(test)]
impl ContainsErr for Result<(), IdentityValidationError> {
fn contains_err(&self, needle: &str) -> bool {
match self {
Err(e) => e.to_string().contains(needle),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_identity_validation_valid() {
let identity = GitIdentity::new("Test User".to_string(), "test@example.com".to_string());
assert!(identity.validate().is_ok());
}
#[test]
fn test_git_identity_validation_empty_name() {
let identity = GitIdentity::new(String::new(), "test@example.com".to_string());
assert!(identity
.validate()
.contains_err("Git user name cannot be empty"));
}
#[test]
fn test_git_identity_validation_empty_email() {
let identity = GitIdentity::new("Test User".to_string(), String::new());
assert!(identity
.validate()
.contains_err("Git user email cannot be empty"));
}
#[test]
fn test_git_identity_validation_invalid_email_no_at() {
let identity = GitIdentity::new("Test User".to_string(), "invalidemail".to_string());
assert!(identity.validate().contains_err("Invalid email format"));
}
#[test]
fn test_git_identity_validation_invalid_email_no_domain() {
let identity = GitIdentity::new("Test User".to_string(), "user@".to_string());
assert!(identity.validate().contains_err("Invalid email format"));
}
#[test]
fn test_fallback_username_not_empty() {
let executor = RealProcessExecutor::new();
let username = fallback_username(Some(&executor));
assert!(!username.is_empty());
}
#[test]
fn test_fallback_email_format() {
let username = "testuser";
let executor = RealProcessExecutor::new();
let email = fallback_email(username, Some(&executor));
assert!(email.contains('@'));
assert!(email.starts_with(username));
}
#[test]
fn test_fallback_username_without_executor() {
let username = fallback_username(None);
assert!(!username.is_empty());
}
#[test]
fn test_fallback_email_without_executor() {
let username = "testuser";
let email = fallback_email(username, None);
assert!(email.contains('@'));
assert!(email.starts_with(username));
}
#[test]
fn test_default_identity() {
let identity = default_identity();
assert_eq!(identity.name, "Ralph Workflow");
assert_eq!(identity.email, "ralph@localhost");
}
#[test]
fn test_validate_empty_name_returns_empty_name_variant() {
let result = validate_git_identity_fields("", "test@example.com");
assert_eq!(result, Err(IdentityValidationError::EmptyName));
}
#[test]
fn test_validate_empty_email_returns_empty_email_variant() {
let result = validate_git_identity_fields("Test User", "");
assert_eq!(result, Err(IdentityValidationError::EmptyEmail));
}
#[test]
fn test_validate_email_no_at_returns_invalid_format_variant() {
let result = validate_git_identity_fields("Test User", "invalidemail");
assert!(
matches!(result, Err(IdentityValidationError::InvalidEmailFormat(_))),
"expected InvalidEmailFormat, got {result:?}"
);
}
#[test]
fn test_validate_email_no_domain_returns_invalid_format_variant() {
let result = validate_git_identity_fields("Test User", "user@");
assert!(
matches!(result, Err(IdentityValidationError::InvalidEmailFormat(_))),
"expected InvalidEmailFormat, got {result:?}"
);
}
#[test]
fn test_validate_identity_error_display_empty_name() {
let err = IdentityValidationError::EmptyName;
assert!(err.to_string().contains("name"));
}
#[test]
fn test_validate_identity_error_display_empty_email() {
let err = IdentityValidationError::EmptyEmail;
assert!(err.to_string().contains("email"));
}
#[test]
fn test_validate_identity_error_display_invalid_format() {
let err = IdentityValidationError::InvalidEmailFormat("bad@".to_string());
assert!(err.to_string().contains("bad@"));
}
}