Skip to main content

git_iris/git/
utils.rs

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