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
43pub 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#[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
74pub 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#[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}