1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::fs;
use std::path::Path;
use crate::ui;
use anyhow::Result;
// wally.lock is deliberately NOT here - like Cargo.lock, it should be
// committed so everyone building the project resolves the same package
// versions. Only the regenerable Packages folder and build artifacts go here.
//
// modules/ is likewise NOT ignored: under the git-submodule workflow it
// holds the generated link files and the submodules project file, which
// are project source and must be committed (the submodule *contents*
// are tracked by git as submodule pointers, not as ignorable files).
const ENTRIES: &[&str] = &[
// Capitalised because that's the folder wally actually creates. Git on
// a case-sensitive filesystem wouldn't ignore it under any other
// spelling, so a Linux contributor would be committing every vendored
// package.
"Packages/",
// Wally's second output folder, for `[server-dependencies]`. Generated
// the same way and just as regenerable, so it's ignored the same way.
"ServerPackages/",
"DevPackages/",
"sourcemap.json",
// Generated place builds and test reports; model assets remain source.
"*.rbxl",
"*.rbxlx",
"*.rbxl.lock",
"*.rbxlx.lock",
"coverage/",
".env",
".env.*",
"!.env.example",
// Local editor state. Note the tradeoff: rproj writes luau-lsp's
// ignoreGlobs here for submodule projects, so a teammate cloning the
// repo won't inherit them and will see the vendored modules/ folder
// linted until they run rproj themselves.
".vscode/",
// Records how *this* checkout was scaffolded (mode, packages, the tools
// present at creation). Useful locally, but it's a snapshot of one
// machine's choices rather than something the project needs to agree
// on - wally.toml, rokit.toml and default.project.json are what
// actually define the project for everyone.
"rproj.toml",
// Fetched fresh by .lute/check.luau each run and deleted afterwards;
// listed so an interrupted run can't leave it staged.
"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 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() {
ui::ok(".gitignore already configured");
return Ok(());
}
let mut updated = content.clone();
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
for entry in &missing {
updated.push_str(entry);
updated.push('\n');
}
fs::write(&path, updated)?;
ui::ok("updated .gitignore");
Ok(())
}
#[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),
("modules/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:?}"
);
}
}
}