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_email(username: &str, executor: Option<&dyn ProcessExecutor>) -> String {
118 let hostname = match get_hostname(executor) {
120 Some(host) if !host.is_empty() => host,
121 _ => "localhost".to_string(),
122 };
123
124 format!("{username}@{hostname}")
125}
126
127fn get_hostname(executor: Option<&dyn ProcessExecutor>) -> Option<String> {
129 if let Ok(hostname) = env::var("HOSTNAME") {
131 let hostname = hostname.trim();
132 if !hostname.is_empty() {
133 return Some(hostname.to_string());
134 }
135 }
136
137 if let Some(exec) = executor {
139 if let Ok(output) = exec.execute("hostname", &[], &[], None) {
140 let hostname = output.stdout.trim().to_string();
141 if !hostname.is_empty() {
142 return Some(hostname);
143 }
144 }
145 }
146
147 None
148}
149
150pub fn default_identity() -> GitIdentity {
154 GitIdentity::new("Ralph Workflow".to_string(), "ralph@localhost".to_string())
155}
156
157#[cfg(test)]
159trait ContainsErr {
160 fn contains_err(&self, needle: &str) -> bool;
161}
162
163#[cfg(test)]
164impl ContainsErr for Result<(), String> {
165 fn contains_err(&self, needle: &str) -> bool {
166 match self {
167 Err(e) => e.contains(needle),
168 _ => false,
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn test_git_identity_validation_valid() {
179 let identity = GitIdentity::new("Test User".to_string(), "test@example.com".to_string());
180 assert!(identity.validate().is_ok());
181 }
182
183 #[test]
184 fn test_git_identity_validation_empty_name() {
185 let identity = GitIdentity::new(String::new(), "test@example.com".to_string());
186 assert!(identity
187 .validate()
188 .contains_err("Git user name cannot be empty"));
189 }
190
191 #[test]
192 fn test_git_identity_validation_empty_email() {
193 let identity = GitIdentity::new("Test User".to_string(), String::new());
194 assert!(identity
195 .validate()
196 .contains_err("Git user email cannot be empty"));
197 }
198
199 #[test]
200 fn test_git_identity_validation_invalid_email_no_at() {
201 let identity = GitIdentity::new("Test User".to_string(), "invalidemail".to_string());
202 assert!(identity.validate().contains_err("Invalid email format"));
203 }
204
205 #[test]
206 fn test_git_identity_validation_invalid_email_no_domain() {
207 let identity = GitIdentity::new("Test User".to_string(), "user@".to_string());
208 assert!(identity.validate().contains_err("Invalid email format"));
209 }
210
211 #[test]
212 fn test_fallback_username_not_empty() {
213 let executor = RealProcessExecutor::new();
214 let username = fallback_username(Some(&executor));
215 assert!(!username.is_empty());
216 }
217
218 #[test]
219 fn test_fallback_email_format() {
220 let username = "testuser";
221 let executor = RealProcessExecutor::new();
222 let email = fallback_email(username, Some(&executor));
223 assert!(email.contains('@'));
224 assert!(email.starts_with(username));
225 }
226
227 #[test]
228 fn test_fallback_username_without_executor() {
229 let username = fallback_username(None);
230 assert!(!username.is_empty());
231 }
232
233 #[test]
234 fn test_fallback_email_without_executor() {
235 let username = "testuser";
236 let email = fallback_email(username, None);
237 assert!(email.contains('@'));
238 assert!(email.starts_with(username));
239 }
240
241 #[test]
242 fn test_default_identity() {
243 let identity = default_identity();
244 assert_eq!(identity.name, "Ralph Workflow");
245 assert_eq!(identity.email, "ralph@localhost");
246 }
247}