use std::fs;
use std::path::Path;
use crate::ui;
use anyhow::Result;
const ENTRIES: &[&str] = &[
"Packages/",
"ServerPackages/",
"DevPackages/",
"sourcemap.json",
"*.rbxl",
"*.rbxlx",
"*.rbxl.lock",
"*.rbxlx.lock",
"coverage/",
".env",
".env.*",
"!.env.example",
".vscode/",
"rproj.toml",
"roblox.d.luau",
".asphalt-debug/",
".tungsten-debug/",
"*.blend1",
"*.blend2",
"Thumbs.db",
".DS_Store",
];
pub fn ensure_entries(project_dir: &Path) -> Result<()> {
let path = project_dir.join(".gitignore");
let content = if path.exists() {
fs::read_to_string(&path)?
} else {
String::new()
};
let Some(updated) = planned_entries(&content) else {
ui::ok(".gitignore already configured");
return Ok(());
};
fs::write(&path, updated)?;
ui::ok("updated .gitignore");
Ok(())
}
pub(crate) fn planned_entries(content: &str) -> Option<String> {
let existing: std::collections::HashSet<&str> = content.lines().map(str::trim).collect();
let missing: Vec<&str> = ENTRIES
.iter()
.copied()
.filter(|e| !existing.contains(e))
.collect();
if missing.is_empty() {
return None;
}
let mut updated = content.to_owned();
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
for entry in &missing {
updated.push_str(entry);
updated.push('\n');
}
Some(updated)
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
#[test]
fn generated_outputs_are_ignored_but_sources_and_lockfiles_are_trackable() {
let dir = tempfile::tempdir().unwrap();
assert!(
Command::new("git")
.arg("init")
.arg(dir.path())
.output()
.unwrap()
.status
.success()
);
fs::write(dir.path().join(".gitignore"), "custom-output/\n").unwrap();
ensure_entries(dir.path()).unwrap();
let first = fs::read(dir.path().join(".gitignore")).unwrap();
ensure_entries(dir.path()).unwrap();
assert_eq!(fs::read(dir.path().join(".gitignore")).unwrap(), first);
for (path, ignored) in [
("Packages/example/init.luau", true),
("ServerPackages/example/init.luau", true),
("DevPackages/Jest.luau", true),
("sourcemap.json", true),
("game.rbxl", true),
("game.rbxlx.lock", true),
("coverage/report.json", true),
(".env.local", true),
("custom-output/example", true),
(".env.example", false),
("wally.lock", false),
("rokit.toml", false),
("assets/model.rbxm", false),
("custom-source/example.luau", false),
(".lute/check.luau", false),
("jest.config.json", false),
] {
let output = Command::new("git")
.args(["check-ignore", "--no-index", "--quiet", path])
.current_dir(dir.path())
.output()
.unwrap();
assert_eq!(
output.status.code(),
Some(if ignored { 0 } else { 1 }),
"{path}: {output:?}"
);
}
}
}