use std::path::Path;
use std::process::Command;
const COMMON_TEMPLATE: &str = "\
# OS
.DS_Store
Thumbs.db
Desktop.ini
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Environment
.env
.env.local
.env.*.local
# Linthis
.linthis/
";
pub fn check_and_create(
project_root: &Path,
quiet: bool,
is_pre_push: bool,
) -> Option<Vec<String>> {
if !crate::utils::is_git_repo() {
return None;
}
let gitignore_path = project_root.join(".gitignore");
if gitignore_path.exists() {
return None;
}
let markers = detect_project_markers(project_root);
let content = generate_gitignore_content(&markers);
if let Err(e) = std::fs::write(&gitignore_path, &content) {
if !quiet {
eprintln!("Warning: Failed to create .gitignore: {}", e);
}
return None;
}
if !quiet {
eprintln!("\u{26a0} Created .gitignore (was missing)");
}
let violations = if is_pre_push {
check_committed_against_ignores(project_root)
} else {
check_staged_against_ignores(project_root)
};
Some(violations)
}
fn detect_project_markers(project_root: &Path) -> Vec<&'static str> {
let checks: &[(&str, &str)] = &[
("Cargo.toml", "rust"),
("package.json", "node"),
("go.mod", "go"),
("pyproject.toml", "python"),
("setup.py", "python"),
("requirements.txt", "python"),
("Makefile", "cpp"),
("CMakeLists.txt", "cpp"),
("pom.xml", "java"),
("build.gradle", "java"),
("build.gradle.kts", "java"),
("Podfile", "swift"),
("Package.swift", "swift"),
];
let mut seen = std::collections::HashSet::new();
let mut markers = Vec::new();
for (file, lang) in checks {
if project_root.join(file).exists() && seen.insert(*lang) {
markers.push(*lang);
}
}
markers
}
fn generate_gitignore_content(markers: &[&str]) -> String {
let mut content = String::from(COMMON_TEMPLATE);
for lang in markers {
let section = match *lang {
"rust" => "\n# Rust\ntarget/\n",
"node" => "\n# Node.js\nnode_modules/\ndist/\nbuild/\n.next/\n",
"go" => "\n# Go\nvendor/\n",
"python" => "\n# Python\n__pycache__/\n*.pyc\n*.pyo\n.venv/\nvenv/\n*.egg-info/\n",
"cpp" => "\n# C/C++\nbuild/\n*.o\n*.so\n*.dylib\n*.a\n",
"java" => "\n# Java/Kotlin\ntarget/\nbuild/\n*.class\n.gradle/\n",
"swift" => {
"\n# Swift/Xcode\n.build/\nPods/\n*.xcworkspace/\nxcuserdata/\nDerivedData/\n"
}
_ => "",
};
content.push_str(section);
}
content
}
const IGNORE_PATTERNS: &[&str] = &[
"node_modules/",
"__pycache__/",
"target/",
".venv/",
"venv/",
"build/",
"dist/",
".gradle/",
".idea/",
".vscode/",
".linthis/",
".env",
".DS_Store",
"Thumbs.db",
"Pods/",
"DerivedData/",
];
const IGNORE_EXT_PATTERNS: &[&str] = &[".pyc", ".pyo", ".class", ".o", ".so", ".dylib", ".a"];
fn filter_ignorable_files(file_list: &str) -> Vec<String> {
file_list
.lines()
.filter(|line| {
let line = line.trim();
if line.is_empty() {
return false;
}
for pat in IGNORE_PATTERNS {
if line.starts_with(pat) || line.contains(&format!("/{}", pat)) {
return true;
}
if !pat.ends_with('/') && line == *pat {
return true;
}
}
for ext in IGNORE_EXT_PATTERNS {
if line.ends_with(ext) {
return true;
}
}
false
})
.map(|s| s.to_string())
.collect()
}
fn check_staged_against_ignores(project_root: &Path) -> Vec<String> {
let output = match Command::new("git")
.args(["diff", "--cached", "--name-only"])
.current_dir(project_root)
.output()
{
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
filter_ignorable_files(&String::from_utf8_lossy(&output.stdout))
}
fn check_committed_against_ignores(project_root: &Path) -> Vec<String> {
let output = match Command::new("git")
.args(["ls-tree", "-r", "--name-only", "HEAD"])
.current_dir(project_root)
.output()
{
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
filter_ignorable_files(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_gitignore_no_markers() {
let content = generate_gitignore_content(&[]);
assert!(content.contains(".DS_Store"));
assert!(content.contains(".linthis/"));
assert!(!content.contains("node_modules"));
}
#[test]
fn test_generate_gitignore_with_node() {
let content = generate_gitignore_content(&["node"]);
assert!(content.contains("node_modules/"));
assert!(content.contains(".DS_Store"));
}
#[test]
fn test_generate_gitignore_multiple_languages() {
let content = generate_gitignore_content(&["rust", "python"]);
assert!(content.contains("target/"));
assert!(content.contains("__pycache__/"));
}
#[test]
fn test_detect_markers_empty() {
let markers = detect_project_markers(Path::new("/nonexistent"));
assert!(markers.is_empty());
}
}