ralph_workflow/git_helpers/
identity.rs1#![deny(unsafe_code)]
20
21use std::env;
22
23use crate::executor::ProcessExecutor;
24
25#[cfg(test)]
26use crate::executor::RealProcessExecutor;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct GitIdentity {
31 pub name: String,
33 pub email: String,
35}
36
37impl GitIdentity {
38 pub const fn new(name: String, email: String) -> Self {
40 Self { name, email }
41 }
42
43 pub fn validate(&self) -> Result<(), String> {
45 if self.name.trim().is_empty() {
46 return Err("Git user name cannot be empty".to_string());
47 }
48 if self.email.trim().is_empty() {
49 return Err("Git user email cannot be empty".to_string());
50 }
51 let email = self.email.trim();
53 if !email.contains('@') {
54 return Err(format!("Invalid email format: '{email}'"));
55 }
56 let parts: Vec<&str> = email.split('@').collect();
57 if parts.len() != 2 {
58 return Err(format!("Invalid email format: '{email}'"));
59 }
60 if parts[0].trim().is_empty() {
61 return Err(format!(
62 "Invalid email format: '{email}' (missing local part)"
63 ));
64 }
65 if parts[1].trim().is_empty() || !parts[1].contains('.') {
66 return Err(format!("Invalid email format: '{email}' (invalid domain)"));
67 }
68 Ok(())
69 }
70}
71
72pub fn fallback_username(executor: Option<&dyn ProcessExecutor>) -> String {
78 if cfg!(unix) {
80 if let Ok(user) = env::var("USER") {
81 if !user.trim().is_empty() {
82 return user.trim().to_string();
83 }
84 }
85 if let Ok(user) = env::var("LOGNAME") {
86 if !user.trim().is_empty() {
87 return user.trim().to_string();
88 }
89 }
90 } else if cfg!(windows) {
91 if let Ok(user) = env::var("USERNAME") {
92 if !user.trim().is_empty() {
93 return user.trim().to_string();
94 }
95 }
96 }
97
98 if cfg!(unix) {
100 if let Some(exec) = executor {
101 if let Ok(output) = exec.execute("whoami", &[], &[], None) {
102 let username = output.stdout.trim().to_string();
103 if !username.is_empty() {
104 return username;
105 }
106 }
107 }
108 }
109
110 "Unknown User".to_string()
112}
113
114pub fn fallback_username_with_real(executor: &dyn ProcessExecutor) -> String {
119 fallback_username(Some(executor))
120}
121
122pub fn fallback_email(username: &str, executor: Option<&dyn ProcessExecutor>) -> String {
126 let hostname = match get_hostname(executor) {
128 Some(host) if !host.is_empty() => host,
129 _ => "localhost".to_string(),
130 };
131
132 format!("{username}@{hostname}")
133}
134
135pub fn fallback_email_with_real(username: &str, executor: &dyn ProcessExecutor) -> String {
140 fallback_email(username, Some(executor))
141}
142
143fn get_hostname(executor: Option<&dyn ProcessExecutor>) -> Option<String> {
145 if let Ok(hostname) = env::var("HOSTNAME") {
147 let hostname = hostname.trim();
148 if !hostname.is_empty() {
149 return Some(hostname.to_string());
150 }
151 }
152
153 if let Some(exec) = executor {
155 if let Ok(output) = exec.execute("hostname", &[], &[], None) {
156 let hostname = output.stdout.trim().to_string();
157 if !hostname.is_empty() {
158 return Some(hostname);
159 }
160 }
161 }
162
163 None
164}
165
166pub fn default_identity() -> GitIdentity {
170 GitIdentity::new("Ralph Workflow".to_string(), "ralph@localhost".to_string())
171}
172
173#[cfg(test)]
175trait ContainsErr {
176 fn contains_err(&self, needle: &str) -> bool;
177}
178
179#[cfg(test)]
180impl ContainsErr for Result<(), String> {
181 fn contains_err(&self, needle: &str) -> bool {
182 match self {
183 Err(e) => e.contains(needle),
184 _ => false,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_git_identity_validation_valid() {
195 let identity = GitIdentity::new("Test User".to_string(), "test@example.com".to_string());
196 assert!(identity.validate().is_ok());
197 }
198
199 #[test]
200 fn test_git_identity_validation_empty_name() {
201 let identity = GitIdentity::new(String::new(), "test@example.com".to_string());
202 assert!(identity
203 .validate()
204 .contains_err("Git user name cannot be empty"));
205 }
206
207 #[test]
208 fn test_git_identity_validation_empty_email() {
209 let identity = GitIdentity::new("Test User".to_string(), String::new());
210 assert!(identity
211 .validate()
212 .contains_err("Git user email cannot be empty"));
213 }
214
215 #[test]
216 fn test_git_identity_validation_invalid_email_no_at() {
217 let identity = GitIdentity::new("Test User".to_string(), "invalidemail".to_string());
218 assert!(identity.validate().contains_err("Invalid email format"));
219 }
220
221 #[test]
222 fn test_git_identity_validation_invalid_email_no_domain() {
223 let identity = GitIdentity::new("Test User".to_string(), "user@".to_string());
224 assert!(identity.validate().contains_err("Invalid email format"));
225 }
226
227 #[test]
228 fn test_fallback_username_not_empty() {
229 let executor = RealProcessExecutor::new();
230 let username = fallback_username_with_real(&executor);
231 assert!(!username.is_empty());
232 }
233
234 #[test]
235 fn test_fallback_email_format() {
236 let username = "testuser";
237 let executor = RealProcessExecutor::new();
238 let email = fallback_email_with_real(username, &executor);
239 assert!(email.contains('@'));
240 assert!(email.starts_with(username));
241 }
242
243 #[test]
244 fn test_fallback_username_without_executor() {
245 let username = fallback_username(None);
246 assert!(!username.is_empty());
247 }
248
249 #[test]
250 fn test_fallback_email_without_executor() {
251 let username = "testuser";
252 let email = fallback_email(username, None);
253 assert!(email.contains('@'));
254 assert!(email.starts_with(username));
255 }
256
257 #[test]
258 fn test_default_identity() {
259 let identity = default_identity();
260 assert_eq!(identity.name, "Ralph Workflow");
261 assert_eq!(identity.email, "ralph@localhost");
262 }
263}