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