1use std::process::{Command, Stdio};
2use std::str::FromStr;
3use std::{
4 collections::VecDeque,
5 path::{Path, PathBuf},
6};
7use std::{
8 ffi::OsStr,
9 io::{self, Write},
10};
11
12use anyhow::{bail, format_err, Context};
13use clap::Parser;
14use fs_err as fs;
15use fs_err::OpenOptions;
16use memofs::{InMemoryFs, Vfs, VfsSnapshot};
17
18use super::resolve_path;
19
20const GIT_IGNORE_PLACEHOLDER: &str = "gitignore.txt";
21
22static TEMPLATE_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/templates.bincode"));
23
24#[derive(Debug, Parser)]
29pub struct InitCommand {
30 #[clap(default_value = "")]
32 pub path: PathBuf,
33
34 #[clap(long, default_value = "place")]
36 pub kind: InitKind,
37
38 #[clap(long)]
40 pub skip_git: bool,
41}
42
43impl InitCommand {
44 pub fn run(self) -> anyhow::Result<()> {
45 let template = self.kind.template()?;
46
47 let base_path = resolve_path(&self.path)?;
48 fs::create_dir_all(&base_path)?;
49
50 let canonical = fs::canonicalize(&base_path)?;
51 let project_name = canonical
52 .file_name()
53 .and_then(|name| name.to_str())
54 .unwrap_or("new-project");
55
56 let project_params = ProjectParams {
57 name: project_name.to_owned(),
58 };
59
60 println!(
61 "Creating new {:?} project '{}'",
62 self.kind, project_params.name
63 );
64
65 let vfs = Vfs::new(template);
66 vfs.set_watch_enabled(false);
67
68 let mut queue = VecDeque::with_capacity(8);
69 for entry in vfs.read_dir("")? {
70 queue.push_back(entry?.path().to_path_buf())
71 }
72
73 while let Some(mut path) = queue.pop_front() {
74 let metadata = vfs.metadata(&path)?;
75 if metadata.is_dir() {
76 fs_err::create_dir(base_path.join(&path))?;
77 for entry in vfs.read_dir(&path)? {
78 queue.push_back(entry?.path().to_path_buf());
79 }
80 } else {
81 let content = vfs.read_to_string_lf_normalized(&path)?;
82 if let Some(file_stem) = path.file_name().and_then(OsStr::to_str) {
83 if file_stem == GIT_IGNORE_PLACEHOLDER {
84 if self.skip_git {
85 continue;
86 } else {
87 path.set_file_name(".gitignore");
88 }
89 }
90 }
91 write_if_not_exists(
92 &base_path.join(&path),
93 &project_params.render_template(&content),
94 )?;
95 }
96 }
97
98 if !self.skip_git && should_git_init(&base_path) {
99 log::debug!("Initializing Git repository...");
100
101 let status = Command::new("git")
102 .arg("init")
103 .current_dir(&base_path)
104 .status()?;
105
106 if !status.success() {
107 bail!("git init failed: status code {:?}", status.code());
108 }
109 }
110
111 println!("Created project successfully.");
112
113 Ok(())
114 }
115}
116
117#[derive(Debug, Clone, Copy)]
119pub enum InitKind {
120 Place,
122
123 Model,
125
126 Plugin,
128}
129
130impl InitKind {
131 fn template(&self) -> anyhow::Result<InMemoryFs> {
132 let template_path = match self {
133 Self::Place => "place",
134 Self::Model => "model",
135 Self::Plugin => "plugin",
136 };
137
138 let snapshot: VfsSnapshot = bincode::deserialize(TEMPLATE_BINCODE)
139 .context("Rojo's templates were not properly packed into Rojo's binary. This is a bug in Rojo; please file an issue.")?;
140
141 let VfsSnapshot::Dir { mut children } = snapshot else {
142 bail!("Rojo's templates were packed as a file instead of a directory. This is a bug in Rojo; please file an issue.");
143 };
144
145 let template = children.remove(template_path).ok_or_else(|| {
146 format_err!(
147 "The template for project type {:?} is missing. This is a bug in Rojo; please file an issue.",
148 self
149 )
150 })?;
151
152 let mut fs = InMemoryFs::new();
153 fs.load_snapshot("", template)
154 .context("Failed to load Rojo's bundled template into memory")?;
155
156 Ok(fs)
157 }
158}
159
160impl FromStr for InitKind {
161 type Err = anyhow::Error;
162
163 fn from_str(source: &str) -> Result<Self, Self::Err> {
164 match source {
165 "place" => Ok(InitKind::Place),
166 "model" => Ok(InitKind::Model),
167 "plugin" => Ok(InitKind::Plugin),
168 _ => Err(format_err!(
169 "Invalid init kind '{}'. Valid kinds are: place, model, plugin",
170 source
171 )),
172 }
173 }
174}
175
176struct ProjectParams {
178 name: String,
179}
180
181impl ProjectParams {
182 fn render_template(&self, template: &str) -> String {
184 template
185 .replace("{project_name}", &self.name)
186 .replace("{rojo_version}", env!("CARGO_PKG_VERSION"))
187 }
188}
189
190fn should_git_init(path: &Path) -> bool {
195 let result = Command::new("git")
196 .args(["rev-parse", "--is-inside-work-tree"])
197 .stdout(Stdio::null())
198 .stderr(Stdio::null())
199 .current_dir(path)
200 .status();
201
202 match result {
203 Ok(status) => !status.success(),
206
207 Err(_) => false,
209 }
210}
211
212fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> {
214 let file_res = OpenOptions::new().write(true).create_new(true).open(path);
215
216 let mut file = match file_res {
217 Ok(file) => file,
218 Err(err) => {
219 return match err.kind() {
220 io::ErrorKind::AlreadyExists => return Ok(()),
221 _ => Err(err.into()),
222 }
223 }
224 };
225
226 file.write_all(contents.as_bytes())?;
227
228 Ok(())
229}