git_editor/utils/
git_config.rs

1use std::path::PathBuf;
2use std::process::Command;
3
4// Attempts to get git configuration values for user name and email. First tries the git command, then falls back to reading ~/.gitconfig directly.
5pub fn get_git_user_name() -> Option<String> {
6    // Try git command first
7    if let Ok(output) = Command::new("git")
8        .args(["config", "--global", "user.name"])
9        .output()
10    {
11        if output.status.success() {
12            let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
13            if !name.is_empty() {
14                return Some(name);
15            }
16        }
17    }
18
19    // Fallback to reading ~/.gitconfig file
20    read_gitconfig_value("user", "name")
21}
22
23// Attempts to get git configuration values for user email. First tries the git command, then falls back to reading ~/.gitconfig directly.
24pub fn get_git_user_email() -> Option<String> {
25    // Try git command first
26    if let Ok(output) = Command::new("git")
27        .args(["config", "--global", "user.email"])
28        .output()
29    {
30        if output.status.success() {
31            let email = String::from_utf8_lossy(&output.stdout).trim().to_string();
32            if !email.is_empty() {
33                return Some(email);
34            }
35        }
36    }
37
38    // Fallback to reading ~/.gitconfig file
39    read_gitconfig_value("user", "email")
40}
41
42// Reads a specific value from the git config file directly. This is used as a fallback when the git command is not available. Handles cross-platform git config locations.
43fn read_gitconfig_value(section: &str, key: &str) -> Option<String> {
44    use std::fs;
45
46    // Get the appropriate git config path for the current OS
47    let gitconfig_paths = get_gitconfig_paths();
48
49    // Try each possible gitconfig path
50    for gitconfig_path in gitconfig_paths {
51        if let Ok(content) = fs::read_to_string(&gitconfig_path) {
52            if let Some(value) = parse_gitconfig(&content, section, key) {
53                return Some(value);
54            }
55        }
56    }
57
58    None
59}
60
61// Returns the possible git config file paths for the current operating system. Returns them in order of precedence (user config first, then system config).
62fn get_gitconfig_paths() -> Vec<PathBuf> {
63    let mut paths = Vec::new();
64
65    // User-level git config (highest precedence)
66    if let Some(user_config) = get_user_gitconfig_path() {
67        paths.push(user_config);
68    }
69
70    // System-level git config (lower precedence)
71    if let Some(system_config) = get_system_gitconfig_path() {
72        paths.push(system_config);
73    }
74
75    paths
76}
77
78// Gets the user-level git config path for the current OS.
79fn get_user_gitconfig_path() -> Option<PathBuf> {
80    // Try different environment variables for home directory
81    let home_dir = if let Ok(home) = std::env::var("HOME") {
82        // Unix-like systems (Linux, macOS)
83        PathBuf::from(home)
84    } else if let Ok(userprofile) = std::env::var("USERPROFILE") {
85        // Windows
86        PathBuf::from(userprofile)
87    } else if let Ok(homedrive) = std::env::var("HOMEDRIVE") {
88        if let Ok(homepath) = std::env::var("HOMEPATH") {
89            // Alternative Windows approach
90            PathBuf::from(homedrive).join(homepath)
91        } else {
92            return None;
93        }
94    } else {
95        return None;
96    };
97
98    Some(home_dir.join(".gitconfig"))
99}
100
101// Gets the system-level git config path for the current OS.
102fn get_system_gitconfig_path() -> Option<PathBuf> {
103    if cfg!(windows) {
104        // Windows: Try common Git installation locations
105        let possible_paths = vec![
106            PathBuf::from(r"C:\ProgramData\Git\config"),
107            PathBuf::from(r"C:\Program Files\Git\etc\gitconfig"),
108            PathBuf::from(r"C:\Program Files (x86)\Git\etc\gitconfig"),
109        ];
110
111        // Return the first one that exists
112        for path in possible_paths {
113            if path.exists() {
114                return Some(path);
115            }
116        }
117        None
118    } else {
119        // Unix-like systems (Linux, macOS)
120        Some(PathBuf::from("/etc/gitconfig"))
121    }
122}
123
124// Simple parser for .gitconfig files to extract specific values. Handles basic INI-style format with [section] and key = value pairs.
125fn parse_gitconfig(content: &str, target_section: &str, target_key: &str) -> Option<String> {
126    let mut in_target_section = false;
127
128    for line in content.lines() {
129        let line = line.trim();
130
131        // Skip empty lines and comments
132        if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
133            continue;
134        }
135
136        // Check for section headers
137        if line.starts_with('[') && line.ends_with(']') {
138            let section = &line[1..line.len() - 1];
139            in_target_section = section.trim().eq_ignore_ascii_case(target_section);
140            continue;
141        }
142
143        // If we're in the target section, look for the key
144        if in_target_section {
145            if let Some(eq_pos) = line.find('=') {
146                let key = line[..eq_pos].trim();
147                if key.eq_ignore_ascii_case(target_key) {
148                    let value = line[eq_pos + 1..].trim();
149                    // Remove quotes if present
150                    let value = if (value.starts_with('"') && value.ends_with('"'))
151                        || (value.starts_with('\'') && value.ends_with('\''))
152                    {
153                        &value[1..value.len() - 1]
154                    } else {
155                        value
156                    };
157                    return Some(value.to_string());
158                }
159            }
160        }
161    }
162
163    None
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_parse_gitconfig_basic() {
172        let config = r#"
173[user]
174    name = John Doe
175    email = john@example.com
176
177[core]
178    editor = vim
179"#;
180
181        assert_eq!(
182            parse_gitconfig(config, "user", "name"),
183            Some("John Doe".to_string())
184        );
185        assert_eq!(
186            parse_gitconfig(config, "user", "email"),
187            Some("john@example.com".to_string())
188        );
189        assert_eq!(
190            parse_gitconfig(config, "core", "editor"),
191            Some("vim".to_string())
192        );
193        assert_eq!(parse_gitconfig(config, "user", "nonexistent"), None);
194        assert_eq!(parse_gitconfig(config, "nonexistent", "name"), None);
195    }
196
197    #[test]
198    fn test_parse_gitconfig_with_quotes() {
199        let config = r#"
200[user]
201    name = "John Doe"
202    email = 'john@example.com'
203"#;
204
205        assert_eq!(
206            parse_gitconfig(config, "user", "name"),
207            Some("John Doe".to_string())
208        );
209        assert_eq!(
210            parse_gitconfig(config, "user", "email"),
211            Some("john@example.com".to_string())
212        );
213    }
214
215    #[test]
216    fn test_parse_gitconfig_case_insensitive() {
217        let config = r#"
218[USER]
219    NAME = John Doe
220    EMAIL = john@example.com
221"#;
222
223        assert_eq!(
224            parse_gitconfig(config, "user", "name"),
225            Some("John Doe".to_string())
226        );
227        assert_eq!(
228            parse_gitconfig(config, "user", "email"),
229            Some("john@example.com".to_string())
230        );
231    }
232
233    #[test]
234    fn test_parse_gitconfig_with_comments() {
235        let config = r#"
236# This is a comment
237[user]
238    name = John Doe  # inline comment
239    ; This is also a comment
240    email = john@example.com
241"#;
242
243        assert_eq!(
244            parse_gitconfig(config, "user", "name"),
245            Some("John Doe  # inline comment".to_string())
246        );
247        assert_eq!(
248            parse_gitconfig(config, "user", "email"),
249            Some("john@example.com".to_string())
250        );
251    }
252
253    #[test]
254    fn test_parse_gitconfig_empty_values() {
255        let config = r#"
256[user]
257    name =
258    email = john@example.com
259"#;
260
261        assert_eq!(
262            parse_gitconfig(config, "user", "name"),
263            Some("".to_string())
264        );
265        assert_eq!(
266            parse_gitconfig(config, "user", "email"),
267            Some("john@example.com".to_string())
268        );
269    }
270
271    #[test]
272    fn test_get_git_user_functions_exist() {
273        // These functions should not panic and should return Option values
274        let _name = get_git_user_name();
275        let _email = get_git_user_email();
276    }
277
278    #[test]
279    fn test_get_user_gitconfig_path() {
280        // Test that the function returns a path
281        let path = get_user_gitconfig_path();
282
283        // Should return Some path on all supported platforms
284        assert!(path.is_some(), "Should return a gitconfig path");
285
286        let path = path.unwrap();
287        assert!(
288            path.ends_with(".gitconfig"),
289            "Path should end with .gitconfig"
290        );
291    }
292
293    #[test]
294    fn test_get_gitconfig_paths() {
295        // Test that we get at least one path back
296        let paths = get_gitconfig_paths();
297        assert!(
298            !paths.is_empty(),
299            "Should return at least one gitconfig path"
300        );
301
302        // First path should be the user config
303        assert!(
304            paths[0].ends_with(".gitconfig"),
305            "First path should be user config"
306        );
307    }
308
309    #[test]
310    fn test_cross_platform_home_detection() {
311        // This test verifies that we can detect home directory on different platforms
312        use std::env;
313
314        // Save original env vars
315        let original_home = env::var("HOME").ok();
316        let original_userprofile = env::var("USERPROFILE").ok();
317        let original_homedrive = env::var("HOMEDRIVE").ok();
318        let original_homepath = env::var("HOMEPATH").ok();
319
320        // Test Unix-like behavior (HOME env var)
321        env::remove_var("USERPROFILE");
322        env::remove_var("HOMEDRIVE");
323        env::remove_var("HOMEPATH");
324        env::set_var("HOME", "/tmp/test-home");
325
326        let path = get_user_gitconfig_path();
327        assert!(path.is_some());
328        let path_buf = path.unwrap();
329        let path_str = path_buf.to_string_lossy();
330        assert!(path_str.contains("test-home"));
331        assert!(path_str.ends_with(".gitconfig"));
332
333        // Test Windows behavior (USERPROFILE env var)
334        env::remove_var("HOME");
335        env::set_var("USERPROFILE", r"C:\Users\TestUser");
336
337        let path = get_user_gitconfig_path();
338        assert!(path.is_some());
339        let path_buf = path.unwrap();
340        let path_str = path_buf.to_string_lossy();
341        assert!(path_str.contains("TestUser"));
342        assert!(path_str.ends_with(".gitconfig"));
343
344        // Test alternative Windows behavior (HOMEDRIVE + HOMEPATH)
345        env::remove_var("USERPROFILE");
346        env::set_var("HOMEDRIVE", "C:");
347        env::set_var("HOMEPATH", r"\Users\TestUser2");
348
349        let path = get_user_gitconfig_path();
350        assert!(path.is_some());
351        let path_buf = path.unwrap();
352        let path_str = path_buf.to_string_lossy();
353        assert!(path_str.contains("TestUser2"));
354        assert!(path_str.ends_with(".gitconfig"));
355
356        // Restore original environment variables
357        env::remove_var("HOME");
358        env::remove_var("USERPROFILE");
359        env::remove_var("HOMEDRIVE");
360        env::remove_var("HOMEPATH");
361
362        if let Some(home) = original_home {
363            env::set_var("HOME", home);
364        }
365        if let Some(userprofile) = original_userprofile {
366            env::set_var("USERPROFILE", userprofile);
367        }
368        if let Some(homedrive) = original_homedrive {
369            env::set_var("HOMEDRIVE", homedrive);
370        }
371        if let Some(homepath) = original_homepath {
372            env::set_var("HOMEPATH", homepath);
373        }
374    }
375
376    #[test]
377    fn test_system_gitconfig_path_logic() {
378        // Test that system config path detection doesn't panic
379        let _path = get_system_gitconfig_path();
380    }
381}