git_iris/git/
utils.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3use std::path::Path;
4use std::process::{Command, Stdio};
5
6use crate::log_debug;
7
8/// Checks if the current directory is inside a Git work tree.
9///
10/// # Returns
11///
12/// A Result containing a boolean indicating if inside a work tree or an error.
13pub fn is_inside_work_tree() -> Result<bool> {
14    let status = Command::new("git")
15        .args(["rev-parse", "--is-inside-work-tree"])
16        .stdout(Stdio::null())
17        .stderr(Stdio::null())
18        .status();
19
20    match status {
21        Ok(exit) => Ok(exit.success()),
22        Err(_) => Ok(false),
23    }
24}
25
26/// Determines if the given diff represents a binary file.
27pub fn is_binary_diff(diff: &str) -> bool {
28    diff.contains("Binary files")
29        || diff.contains("GIT binary patch")
30        || diff.contains("[Binary file changed]")
31}
32
33/// Executes a git command and returns the output as a string
34///
35/// # Arguments
36///
37/// * `args` - The arguments to pass to git
38///
39/// # Returns
40///
41/// A Result containing the output as a String or an error.
42pub fn run_git_command(args: &[&str]) -> Result<String> {
43    let output = Command::new("git")
44        .args(args)
45        .output()
46        .context("Failed to execute git command")?;
47
48    if !output.status.success() {
49        return Err(anyhow::anyhow!(
50            "Git command failed: {}",
51            String::from_utf8_lossy(&output.stderr)
52        ));
53    }
54
55    let stdout =
56        String::from_utf8(output.stdout).context("Invalid UTF-8 output from git command")?;
57
58    Ok(stdout.trim().to_string())
59}
60
61/// Checks if a file should be excluded from analysis.
62///
63/// Excludes common directories and files that don't contribute meaningfully
64/// to commit context (build artifacts, lock files, IDE configs, etc.)
65pub fn should_exclude_file(path: &str) -> bool {
66    log_debug!("Checking if file should be excluded: {}", path);
67    let exclude_patterns = vec![
68        (String::from(r"(^|/)\.git(/|$)"), false), // Only exclude .git directory, not .github
69        (String::from(r"(^|/)\.svn(/|$)"), false),
70        (String::from(r"(^|/)\.hg(/|$)"), false),
71        (String::from(r"(^|/)\.DS_Store$"), false),
72        (String::from(r"(^|/)node_modules(/|$)"), false),
73        (String::from(r"(^|/)target(/|$)"), false),
74        (String::from(r"(^|/)build(/|$)"), false),
75        (String::from(r"(^|/)dist(/|$)"), false),
76        (String::from(r"(^|/)\.vscode(/|$)"), false),
77        (String::from(r"(^|/)\.idea(/|$)"), false),
78        (String::from(r"(^|/)\.vs(/|$)"), false),
79        (String::from(r"package-lock\.json$"), true),
80        (String::from(r"\.lock$"), true),
81        (String::from(r"\.log$"), true),
82        (String::from(r"\.tmp$"), true),
83        (String::from(r"\.temp$"), true),
84        (String::from(r"\.swp$"), true),
85        (String::from(r"\.min\.js$"), true),
86    ];
87
88    let path = Path::new(path);
89
90    for (pattern, is_extension) in exclude_patterns {
91        let re = match Regex::new(&pattern) {
92            Ok(re) => re,
93            Err(e) => {
94                log_debug!("Failed to compile regex '{}': {}", pattern, e);
95                continue;
96            }
97        };
98
99        if is_extension {
100            if let Some(file_name) = path.file_name()
101                && let Some(file_name_str) = file_name.to_str()
102                && re.is_match(file_name_str)
103            {
104                log_debug!("File excluded: {}", path.display());
105                return true;
106            }
107        } else if let Some(path_str) = path.to_str()
108            && re.is_match(path_str)
109        {
110            log_debug!("File excluded: {}", path.display());
111            return true;
112        }
113    }
114    log_debug!("File not excluded: {}", path.display());
115    false
116}