ralph_workflow/git_helpers/
identity.rs1#![deny(unsafe_code)]
20
21use std::env;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct GitIdentity {
26 pub name: String,
28 pub email: String,
30}
31
32impl GitIdentity {
33 pub const fn new(name: String, email: String) -> Self {
35 Self { name, email }
36 }
37
38 pub fn validate(&self) -> Result<(), String> {
40 if self.name.trim().is_empty() {
41 return Err("Git user name cannot be empty".to_string());
42 }
43 if self.email.trim().is_empty() {
44 return Err("Git user email cannot be empty".to_string());
45 }
46 let email = self.email.trim();
48 if !email.contains('@') {
49 return Err(format!("Invalid email format: '{email}'"));
50 }
51 let parts: Vec<&str> = email.split('@').collect();
52 if parts.len() != 2 {
53 return Err(format!("Invalid email format: '{email}'"));
54 }
55 if parts[0].trim().is_empty() {
56 return Err(format!(
57 "Invalid email format: '{email}' (missing local part)"
58 ));
59 }
60 if parts[1].trim().is_empty() || !parts[1].contains('.') {
61 return Err(format!("Invalid email format: '{email}' (invalid domain)"));
62 }
63 Ok(())
64 }
65}
66
67pub fn fallback_username() -> String {
73 if cfg!(unix) {
75 if let Ok(user) = env::var("USER") {
76 if !user.trim().is_empty() {
77 return user.trim().to_string();
78 }
79 }
80 if let Ok(user) = env::var("LOGNAME") {
81 if !user.trim().is_empty() {
82 return user.trim().to_string();
83 }
84 }
85 } else if cfg!(windows) {
86 if let Ok(user) = env::var("USERNAME") {
87 if !user.trim().is_empty() {
88 return user.trim().to_string();
89 }
90 }
91 }
92
93 if cfg!(unix) {
95 if let Ok(output) = std::process::Command::new("whoami").output() {
96 let username = String::from_utf8_lossy(&output.stdout).trim().to_string();
97 if !username.is_empty() {
98 return username;
99 }
100 }
101 }
102
103 "Unknown User".to_string()
105}
106
107pub fn fallback_email(username: &str) -> String {
111 let hostname = match get_hostname() {
113 Some(host) if !host.is_empty() => host,
114 _ => "localhost".to_string(),
115 };
116
117 format!("{username}@{hostname}")
118}
119
120fn get_hostname() -> Option<String> {
122 if let Ok(hostname) = env::var("HOSTNAME") {
124 let hostname = hostname.trim();
125 if !hostname.is_empty() {
126 return Some(hostname.to_string());
127 }
128 }
129
130 if let Ok(output) = std::process::Command::new("hostname").output() {
132 let hostname = String::from_utf8_lossy(&output.stdout).trim().to_string();
133 if !hostname.is_empty() {
134 return Some(hostname);
135 }
136 }
137
138 None
139}
140
141pub fn default_identity() -> GitIdentity {
145 GitIdentity::new("Ralph Workflow".to_string(), "ralph@localhost".to_string())
146}
147
148#[cfg(test)]
150trait ContainsErr {
151 fn contains_err(&self, needle: &str) -> bool;
152}
153
154#[cfg(test)]
155impl ContainsErr for Result<(), String> {
156 fn contains_err(&self, needle: &str) -> bool {
157 match self {
158 Err(e) => e.contains(needle),
159 _ => false,
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_git_identity_validation_valid() {
170 let identity = GitIdentity::new("Test User".to_string(), "test@example.com".to_string());
171 assert!(identity.validate().is_ok());
172 }
173
174 #[test]
175 fn test_git_identity_validation_empty_name() {
176 let identity = GitIdentity::new(String::new(), "test@example.com".to_string());
177 assert!(identity
178 .validate()
179 .contains_err("Git user name cannot be empty"));
180 }
181
182 #[test]
183 fn test_git_identity_validation_empty_email() {
184 let identity = GitIdentity::new("Test User".to_string(), String::new());
185 assert!(identity
186 .validate()
187 .contains_err("Git user email cannot be empty"));
188 }
189
190 #[test]
191 fn test_git_identity_validation_invalid_email_no_at() {
192 let identity = GitIdentity::new("Test User".to_string(), "invalidemail".to_string());
193 assert!(identity.validate().contains_err("Invalid email format"));
194 }
195
196 #[test]
197 fn test_git_identity_validation_invalid_email_no_domain() {
198 let identity = GitIdentity::new("Test User".to_string(), "user@".to_string());
199 assert!(identity.validate().contains_err("Invalid email format"));
200 }
201
202 #[test]
203 fn test_fallback_username_not_empty() {
204 let username = fallback_username();
205 assert!(!username.is_empty());
206 }
207
208 #[test]
209 fn test_fallback_email_format() {
210 let username = "testuser";
211 let email = fallback_email(username);
212 assert!(email.contains('@'));
213 assert!(email.starts_with(username));
214 }
215
216 #[test]
217 fn test_default_identity() {
218 let identity = default_identity();
219 assert_eq!(identity.name, "Ralph Workflow");
220 assert_eq!(identity.email, "ralph@localhost");
221 }
222}