Skip to main content

ai_agent/utils/git/
gitignore.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/git/gitignore.ts
2use std::io::Write;
3use std::path::Path;
4use std::process::Command;
5
6/// Checks if a path is ignored by git (via `git check-ignore`).
7pub fn is_path_gitignored<P: AsRef<std::path::Path>, C: AsRef<std::path::Path>>(file_path: P, cwd: C) -> bool {
8    let output = Command::new("git")
9        .args(["check-ignore", file_path.as_ref().to_str().unwrap_or("")])
10        .current_dir(cwd.as_ref())
11        .output();
12
13    match output {
14        Ok(o) => o.status.code() == Some(0),
15        Err(_) => false,
16    }
17}
18
19/// Gets the path to the global gitignore file (.config/git/ignore)
20pub fn get_global_gitignore_path() -> String {
21    if let Some(home) = std::env::var("HOME").ok() {
22        format!("{}/.config/git/ignore", home)
23    } else {
24        ".config/git/ignore".to_string()
25    }
26}
27
28/// Checks if a directory is inside a git repo.
29pub fn dir_is_in_git_repo(dir: &str) -> bool {
30    Command::new("git")
31        .args(["rev-parse", "--git-dir"])
32        .current_dir(dir)
33        .output()
34        .map(|o| o.status.success())
35        .unwrap_or(false)
36}
37
38/// Adds a file pattern to the global gitignore file.
39pub async fn add_file_glob_rule_to_gitignore(file_name: &str, cwd: &str) {
40    if let Err(e) = add_file_glob_rule_inner(file_name, cwd).await {
41        eprintln!("Failed to add gitignore rule: {}", e);
42    }
43}
44
45async fn add_file_glob_rule_inner(file_name: &str, cwd: &str) -> std::io::Result<()> {
46    if !dir_is_in_git_repo(cwd) {
47        return Ok(());
48    }
49
50    let gitignore_entry = format!("**/{}", file_name);
51    let test_path = if file_name.ends_with('/') {
52        format!("{}sample-file.txt", file_name)
53    } else {
54        file_name.to_string()
55    };
56
57    if is_path_gitignored(&test_path, cwd) {
58        return Ok(());
59    }
60
61    let global_gitignore_path = get_global_gitignore_path();
62
63    if let Some(parent) = Path::new(&global_gitignore_path).parent() {
64        std::fs::create_dir_all(parent)?;
65    }
66
67    if let Ok(content) = std::fs::read_to_string(&global_gitignore_path) {
68        if content.contains(&gitignore_entry) {
69            return Ok(());
70        }
71        std::fs::OpenOptions::new()
72            .append(true)
73            .open(&global_gitignore_path)?
74            .write_all(format!("\n{}\n", gitignore_entry).as_bytes())?;
75    } else {
76        std::fs::write(&global_gitignore_path, format!("{}\n", gitignore_entry))?;
77    }
78
79    Ok(())
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_is_path_gitignored_not_in_repo() {
88        assert!(!is_path_gitignored("some_file.txt", "/tmp"));
89    }
90
91    #[test]
92    fn test_get_global_gitignore_path() {
93        let path = get_global_gitignore_path();
94        assert!(path.contains(".config/git/ignore"));
95    }
96}