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 #[must_use]
40 pub const fn new(name: String, email: String) -> Self {
41 Self { name, email }
42 }
43
44 pub fn validate(&self) -> Result<(), String> {
50 if self.name.trim().is_empty() {
51 return Err("Git user name cannot be empty".to_string());
52 }
53 if self.email.trim().is_empty() {
54 return Err("Git user email cannot be empty".to_string());
55 }
56 let email = self.email.trim();
58 if !email.contains('@') {
59 return Err(format!("Invalid email format: '{email}'"));
60 }
61 let parts: Vec<&str> = email.split('@').collect();
62 if parts.len() != 2 {
63 return Err(format!("Invalid email format: '{email}'"));
64 }
65 if parts[0].trim().is_empty() {
66 return Err(format!(
67 "Invalid email format: '{email}' (missing local part)"
68 ));
69 }
70 if parts[1].trim().is_empty() || !parts[1].contains('.') {
71 return Err(format!("Invalid email format: '{email}' (invalid domain)"));
72 }
73 Ok(())
74 }
75}
76
77#[must_use]
83pub fn fallback_username(executor: Option<&dyn ProcessExecutor>) -> String {
84 if cfg!(unix) {
86 if let Ok(user) = env::var("USER") {
87 if !user.trim().is_empty() {
88 return user.trim().to_string();
89 }
90 }
91 if let Ok(user) = env::var("LOGNAME") {
92 if !user.trim().is_empty() {
93 return user.trim().to_string();
94 }
95 }
96 } else if cfg!(windows) {
97 if let Ok(user) = env::var("USERNAME") {
98 if !user.trim().is_empty() {
99 return user.trim().to_string();
100 }
101 }
102 }
103
104 if cfg!(unix) {
106 if let Some(exec) = executor {
107 if let Ok(output) = exec.execute("whoami", &[], &[], None) {
108 let username = output.stdout.trim().to_string();
109 if !username.is_empty() {
110 return username;
111 }
112 }
113 }
114 }
115
116 "Unknown User".to_string()
118}
119
120#[must_use]
124pub fn fallback_email(username: &str, executor: Option<&dyn ProcessExecutor>) -> String {
125 let hostname = match get_hostname(executor) {
127 Some(host) if !host.is_empty() => host,
128 _ => "localhost".to_string(),
129 };
130
131 format!("{username}@{hostname}")
132}
133
134fn get_hostname(executor: Option<&dyn ProcessExecutor>) -> Option<String> {
136 if let Ok(hostname) = env::var("HOSTNAME") {
138 let hostname = hostname.trim();
139 if !hostname.is_empty() {
140 return Some(hostname.to_string());
141 }
142 }
143
144 if let Some(exec) = executor {
146 if let Ok(output) = exec.execute("hostname", &[], &[], None) {
147 let hostname = output.stdout.trim().to_string();
148 if !hostname.is_empty() {
149 return Some(hostname);
150 }
151 }
152 }
153
154 None
155}
156
157#[must_use]
161pub fn default_identity() -> GitIdentity {
162 GitIdentity::new("Ralph Workflow".to_string(), "ralph@localhost".to_string())
163}
164
165#[cfg(test)]
167trait ContainsErr {
168 fn contains_err(&self, needle: &str) -> bool;
169}
170
171#[cfg(test)]
172impl ContainsErr for Result<(), String> {
173 fn contains_err(&self, needle: &str) -> bool {
174 match self {
175 Err(e) => e.contains(needle),
176 _ => false,
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_git_identity_validation_valid() {
187 let identity = GitIdentity::new("Test User".to_string(), "test@example.com".to_string());
188 assert!(identity.validate().is_ok());
189 }
190
191 #[test]
192 fn test_git_identity_validation_empty_name() {
193 let identity = GitIdentity::new(String::new(), "test@example.com".to_string());
194 assert!(identity
195 .validate()
196 .contains_err("Git user name cannot be empty"));
197 }
198
199 #[test]
200 fn test_git_identity_validation_empty_email() {
201 let identity = GitIdentity::new("Test User".to_string(), String::new());
202 assert!(identity
203 .validate()
204 .contains_err("Git user email cannot be empty"));
205 }
206
207 #[test]
208 fn test_git_identity_validation_invalid_email_no_at() {
209 let identity = GitIdentity::new("Test User".to_string(), "invalidemail".to_string());
210 assert!(identity.validate().contains_err("Invalid email format"));
211 }
212
213 #[test]
214 fn test_git_identity_validation_invalid_email_no_domain() {
215 let identity = GitIdentity::new("Test User".to_string(), "user@".to_string());
216 assert!(identity.validate().contains_err("Invalid email format"));
217 }
218
219 #[test]
220 fn test_fallback_username_not_empty() {
221 let executor = RealProcessExecutor::new();
222 let username = fallback_username(Some(&executor));
223 assert!(!username.is_empty());
224 }
225
226 #[test]
227 fn test_fallback_email_format() {
228 let username = "testuser";
229 let executor = RealProcessExecutor::new();
230 let email = fallback_email(username, Some(&executor));
231 assert!(email.contains('@'));
232 assert!(email.starts_with(username));
233 }
234
235 #[test]
236 fn test_fallback_username_without_executor() {
237 let username = fallback_username(None);
238 assert!(!username.is_empty());
239 }
240
241 #[test]
242 fn test_fallback_email_without_executor() {
243 let username = "testuser";
244 let email = fallback_email(username, None);
245 assert!(email.contains('@'));
246 assert!(email.starts_with(username));
247 }
248
249 #[test]
250 fn test_default_identity() {
251 let identity = default_identity();
252 assert_eq!(identity.name, "Ralph Workflow");
253 assert_eq!(identity.email, "ralph@localhost");
254 }
255}