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};
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 && !self.skip_git {
84 path.set_file_name(".gitignore");
85 }
86 }
87 write_if_not_exists(
88 &base_path.join(&path),
89 &project_params.render_template(&content),
90 )?;
91 }
92 }
93
94 if !self.skip_git && should_git_init(&base_path) {
95 log::debug!("Initializing Git repository...");
96
97 let status = Command::new("git")
98 .arg("init")
99 .current_dir(&base_path)
100 .status()?;
101
102 if !status.success() {
103 bail!("git init failed: status code {:?}", status.code());
104 }
105 }
106
107 println!("Created project successfully.");
108
109 Ok(())
110 }
111}
112
113#[derive(Debug, Clone, Copy)]
115pub enum InitKind {
116 Place,
118
119 Model,
121
122 Plugin,
124}
125
126impl InitKind {
127 fn template(&self) -> InMemoryFs {
128 let template_path = match self {
129 Self::Place => "place",
130 Self::Model => "model",
131 Self::Plugin => "plugin",
132 };
133
134 let snapshot: VfsSnapshot = bincode::deserialize(TEMPLATE_BINCODE)
135 .expect("Rojo's templates were not properly packed into Rojo's binary");
136
137 if let VfsSnapshot::Dir { mut children } = snapshot {
138 if let Some(template) = children.remove(template_path) {
139 let mut fs = InMemoryFs::new();
140 fs.load_snapshot("", template)
141 .expect("loading a template in memory should never fail");
142 fs
143 } else {
144 panic!("template for project type {:?} is missing", self)
145 }
146 } else {
147 panic!("Rojo's templates were packed as a file instead of a directory")
148 }
149 }
150}
151
152impl FromStr for InitKind {
153 type Err = anyhow::Error;
154
155 fn from_str(source: &str) -> Result<Self, Self::Err> {
156 match source {
157 "place" => Ok(InitKind::Place),
158 "model" => Ok(InitKind::Model),
159 "plugin" => Ok(InitKind::Plugin),
160 _ => Err(format_err!(
161 "Invalid init kind '{}'. Valid kinds are: place, model, plugin",
162 source
163 )),
164 }
165 }
166}
167
168struct ProjectParams {
170 name: String,
171}
172
173impl ProjectParams {
174 fn render_template(&self, template: &str) -> String {
176 template
177 .replace("{project_name}", &self.name)
178 .replace("{rojo_version}", env!("CARGO_PKG_VERSION"))
179 }
180}
181
182fn should_git_init(path: &Path) -> bool {
187 let result = Command::new("git")
188 .args(["rev-parse", "--is-inside-work-tree"])
189 .stdout(Stdio::null())
190 .stderr(Stdio::null())
191 .current_dir(path)
192 .status();
193
194 match result {
195 Ok(status) => !status.success(),
198
199 Err(_) => false,
201 }
202}
203
204fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> {
206 let file_res = OpenOptions::new().write(true).create_new(true).open(path);
207
208 let mut file = match file_res {
209 Ok(file) => file,
210 Err(err) => {
211 return match err.kind() {
212 io::ErrorKind::AlreadyExists => return Ok(()),
213 _ => Err(err.into()),
214 }
215 }
216 };
217
218 file.write_all(contents.as_bytes())?;
219
220 Ok(())
221}