1use anyhow::{bail, Context};
4use std::{
5 env,
6 path::{Path, PathBuf},
7 process::Command,
8 sync::OnceLock,
9};
10
11static GIT_DIR_CACHE: OnceLock<PathBuf> = OnceLock::new();
13
14static GIT_COMMON_DIR_CACHE: OnceLock<PathBuf> = OnceLock::new();
16
17static REPO_ROOT_CACHE: OnceLock<PathBuf> = OnceLock::new();
19
20fn run_git_command(args: &[&str]) -> anyhow::Result<String> {
22 let output = Command::new("git")
23 .args(args)
24 .output()
25 .with_context(|| "Git not found in PATH")?;
26
27 if !output.status.success() {
28 let stderr = String::from_utf8_lossy(&output.stderr);
29 let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
30
31 bail!(
32 "Not in a git repository (cwd: {}): {}",
33 cwd.display(),
34 stderr.trim()
35 );
36 }
37
38 let stdout = String::from_utf8_lossy(&output.stdout);
39 let result = stdout.trim();
40
41 if result.is_empty() {
42 bail!("Git command returned empty output: git {}", args.join(" "));
43 }
44
45 Ok(result.to_string())
46}
47
48fn validate_git_path(path: &Path) -> anyhow::Result<PathBuf> {
50 let resolved = path
51 .canonicalize()
52 .with_context(|| format!("Invalid path returned by git: {}", path.display()))?;
53
54 Ok(resolved)
55}
56
57fn cached_git_path(
60 cache: &OnceLock<PathBuf>,
61 rev_parse_arg: &str,
62 error_context: &'static str,
63) -> anyhow::Result<PathBuf> {
64 if let Some(cached) = cache.get() {
65 return Ok(cached.clone());
66 }
67
68 let output = run_git_command(&["rev-parse", rev_parse_arg]).context(error_context)?;
69 let path = PathBuf::from(output);
70 let validated = validate_git_path(&path)?;
71
72 let _ = cache.set(validated.clone());
74 Ok(validated)
75}
76
77pub fn get_git_dir() -> anyhow::Result<PathBuf> {
85 cached_git_path(
86 &GIT_DIR_CACHE,
87 "--absolute-git-dir",
88 "Failed to find git directory",
89 )
90}
91
92pub fn get_git_common_dir() -> anyhow::Result<PathBuf> {
100 cached_git_path(
101 &GIT_COMMON_DIR_CACHE,
102 "--git-common-dir",
103 "Failed to find git common directory",
104 )
105}
106
107pub fn get_repo_root() -> anyhow::Result<PathBuf> {
109 cached_git_path(
110 &REPO_ROOT_CACHE,
111 "--show-toplevel",
112 "Failed to find repository root",
113 )
114}
115
116pub fn get_global_gitignore_path() -> Option<PathBuf> {
118 if let Ok(output) = run_git_command(&["config", "--global", "core.excludesfile"]) {
120 let path = PathBuf::from(output);
121 let expanded = if path.starts_with("~") {
122 let home = env::var_os("HOME")?;
123 PathBuf::from(home).join(path.strip_prefix("~").unwrap())
124 } else if !path.is_absolute() {
125 let home = env::var_os("HOME")?;
126 PathBuf::from(home).join(&path)
127 } else {
128 path
129 };
130
131 if expanded.exists() {
132 return Some(expanded);
133 }
134 }
135
136 if let Some(xdg_config) = env::var_os("XDG_CONFIG_HOME") {
138 let path = PathBuf::from(xdg_config).join("git").join("ignore");
139 if path.exists() {
140 return Some(path);
141 }
142 }
143
144 if let Some(home) = env::var_os("HOME") {
145 let home_path = PathBuf::from(&home);
146
147 let path = home_path.join(".config").join("git").join("ignore");
148 if path.exists() {
149 return Some(path);
150 }
151
152 let path = home_path.join(".gitignore_global");
153 if path.exists() {
154 return Some(path);
155 }
156
157 let path = home_path.join(".gitignore");
158 if path.exists() {
159 return Some(path);
160 }
161 }
162
163 None
164}
165
166pub fn get_exclude_file_path() -> anyhow::Result<PathBuf> {
168 let git_common_dir = get_git_common_dir()?;
169 Ok(git_common_dir.join("info").join("exclude"))
170}
171
172pub fn get_gitignore_path() -> anyhow::Result<PathBuf> {
174 let repo_root = get_repo_root()?;
175 Ok(repo_root.join(".gitignore"))
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::env;
182
183 #[test]
184 fn test_run_git_command_failure() {
185 let result = run_git_command(&["nonexistent-command"]);
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn test_validate_git_path() {
191 let current_dir = env::current_dir().unwrap();
192 let result = validate_git_path(¤t_dir);
193 assert!(result.is_ok());
194
195 let invalid_path = Path::new("/nonexistent/path/that/should/not/exist");
196 let result = validate_git_path(invalid_path);
197 assert!(result.is_err());
198 }
199
200 #[test]
201 fn test_get_global_gitignore_path() {
202 let _ = get_global_gitignore_path();
205 }
206}