mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
- Added `GIT_TERMINAL_PROMPT` environment variable set to `0` to suppress terminal prompts during Git operations. - Redirected standard input to null to prevent any interactive prompts, ensuring smoother initialization processes. These changes aim to enhance the user experience by streamlining the Git initialization workflow.
333 lines
10 KiB
Rust
333 lines
10 KiB
Rust
use std::process::{Command, Stdio};
|
|
use std::str::FromStr;
|
|
use std::{
|
|
collections::VecDeque,
|
|
path::{Path, PathBuf},
|
|
};
|
|
use std::{
|
|
ffi::OsStr,
|
|
io::{self, Write},
|
|
};
|
|
|
|
use anyhow::{bail, format_err};
|
|
use clap::Parser;
|
|
use fs_err as fs;
|
|
use fs_err::OpenOptions;
|
|
use memofs::{InMemoryFs, Vfs, VfsSnapshot};
|
|
|
|
use super::resolve_path;
|
|
|
|
const GIT_IGNORE_PLACEHOLDER: &str = "gitignore.txt";
|
|
|
|
static TEMPLATE_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/templates.bincode"));
|
|
|
|
/// Initializes a new Rojo project.
|
|
///
|
|
/// By default, this will attempt to initialize a 'git' repository in the
|
|
/// project directory if `git` is installed. To avoid this, pass `--skip-git`.
|
|
#[derive(Debug, Parser)]
|
|
pub struct InitCommand {
|
|
/// Path to the place to create the project. Defaults to the current directory.
|
|
#[clap(long, default_value = ".")]
|
|
pub path: PathBuf,
|
|
|
|
/// The kind of project to create, 'place', 'plugin', or 'model'.
|
|
#[clap(long, default_value = "place")]
|
|
pub kind: InitKind,
|
|
|
|
/// Skips the initialization of a git repository.
|
|
#[clap(long)]
|
|
pub skip_git: bool,
|
|
|
|
/// Place ID to use for servePlaceIds.
|
|
pub placeid: Option<u64>,
|
|
|
|
/// Skip cloning cursor rules into .cursor directory.
|
|
#[clap(long)]
|
|
pub skip_rules: bool,
|
|
}
|
|
|
|
impl InitCommand {
|
|
pub fn run(self) -> anyhow::Result<()> {
|
|
let template = self.kind.template();
|
|
|
|
let base_path = resolve_path(&self.path);
|
|
|
|
// Check if directory exists and is non-empty
|
|
if base_path.exists() {
|
|
let is_empty = base_path.read_dir()?.next().is_none();
|
|
if !is_empty {
|
|
bail!(
|
|
"Directory '{}' is not empty. Please use an empty directory.",
|
|
base_path.display()
|
|
);
|
|
}
|
|
}
|
|
|
|
fs::create_dir_all(&base_path)?;
|
|
|
|
let canonical = fs::canonicalize(&base_path)?;
|
|
let project_name = canonical
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.unwrap_or("new-project");
|
|
|
|
let project_params = ProjectParams {
|
|
name: project_name.to_owned(),
|
|
place_id: self.placeid,
|
|
};
|
|
|
|
println!(
|
|
"Creating new {:?} project '{}'",
|
|
self.kind, project_params.name
|
|
);
|
|
|
|
let vfs = Vfs::new(template);
|
|
vfs.set_watch_enabled(false);
|
|
|
|
let mut queue = VecDeque::with_capacity(8);
|
|
for entry in vfs.read_dir("")? {
|
|
queue.push_back(entry?.path().to_path_buf())
|
|
}
|
|
|
|
while let Some(mut path) = queue.pop_front() {
|
|
let metadata = vfs.metadata(&path)?;
|
|
if metadata.is_dir() {
|
|
fs_err::create_dir(base_path.join(&path))?;
|
|
for entry in vfs.read_dir(&path)? {
|
|
queue.push_back(entry?.path().to_path_buf());
|
|
}
|
|
} else {
|
|
let content = vfs.read_to_string_lf_normalized(&path)?;
|
|
if let Some(file_stem) = path.file_name().and_then(OsStr::to_str) {
|
|
if file_stem == GIT_IGNORE_PLACEHOLDER {
|
|
if self.skip_git {
|
|
continue;
|
|
} else {
|
|
path.set_file_name(".gitignore");
|
|
}
|
|
}
|
|
}
|
|
write_if_not_exists(
|
|
&base_path.join(&path),
|
|
&project_params.render_template(&content),
|
|
)?;
|
|
}
|
|
}
|
|
|
|
let did_git_init = if !self.skip_git && should_git_init(&base_path) {
|
|
log::debug!("Initializing Git repository...");
|
|
|
|
let status = Command::new("git")
|
|
.arg("init")
|
|
.current_dir(&base_path)
|
|
.status()?;
|
|
|
|
if !status.success() {
|
|
bail!("git init failed: status code {:?}", status.code());
|
|
}
|
|
|
|
for (key, value) in [
|
|
("core.autocrlf", "false"),
|
|
("core.eol", "lf"),
|
|
("core.safecrlf", "false"),
|
|
// ("core.splitIndex", "true"),
|
|
// ("core.fsmonitor", "true"),
|
|
// ("core.untrackedcache", "true"),
|
|
// ("feature.manyFiles", "true"),
|
|
] {
|
|
let _ = Command::new("git")
|
|
.args(["config", "--local", key, value])
|
|
.current_dir(&base_path)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
}
|
|
|
|
true
|
|
} else {
|
|
!self.skip_git
|
|
};
|
|
|
|
if !self.skip_rules {
|
|
log::debug!("Cloning cursor rules...");
|
|
|
|
let cursor_dir = base_path.join(".cursor");
|
|
let result = Command::new("git")
|
|
.args([
|
|
"clone",
|
|
"--depth",
|
|
"1",
|
|
"https://github.com/jrmelsha/cursor-rules.git",
|
|
".cursor",
|
|
])
|
|
.current_dir(&base_path)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.env("GIT_TERMINAL_PROMPT", "0")
|
|
.status();
|
|
|
|
match result {
|
|
Ok(status) if status.success() => {
|
|
// Remove .git so it's just files, not a submodule
|
|
let git_dir = cursor_dir.join(".git");
|
|
if git_dir.exists() {
|
|
let _ = fs::remove_dir_all(&git_dir);
|
|
}
|
|
println!("Cloned cursor rules successfully.");
|
|
}
|
|
_ => {
|
|
log::debug!("Failed to clone cursor rules, skipping.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if did_git_init {
|
|
log::info!("Staging initial project...");
|
|
let _ = Command::new("git")
|
|
.args(["add", "."])
|
|
.current_dir(&base_path)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
|
|
log::info!("Committing initial project...");
|
|
let _ = Command::new("git")
|
|
.args(["commit", "--no-verify", "-m", "Initial commit"])
|
|
.current_dir(&base_path)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status();
|
|
}
|
|
|
|
println!("Created project successfully.");
|
|
|
|
if self.placeid.is_some() {
|
|
println!("Run 'atlas syncback' to sync your place.");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// The templates we support for initializing a Rojo project.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum InitKind {
|
|
/// A place that contains a baseplate.
|
|
Place,
|
|
|
|
/// An empty model, suitable for a library.
|
|
Model,
|
|
|
|
/// An empty plugin.
|
|
Plugin,
|
|
}
|
|
|
|
impl InitKind {
|
|
fn template(&self) -> InMemoryFs {
|
|
let template_path = match self {
|
|
Self::Place => "place",
|
|
Self::Model => "model",
|
|
Self::Plugin => "plugin",
|
|
};
|
|
|
|
let (snapshot, _): (VfsSnapshot, usize) =
|
|
bincode::serde::decode_from_slice(TEMPLATE_BINCODE, bincode::config::standard())
|
|
.expect("Rojo's templates were not properly packed into Rojo's binary");
|
|
|
|
if let VfsSnapshot::Dir { mut children } = snapshot {
|
|
if let Some(template) = children.remove(template_path) {
|
|
let mut fs = InMemoryFs::new();
|
|
fs.load_snapshot("", template)
|
|
.expect("loading a template in memory should never fail");
|
|
fs
|
|
} else {
|
|
panic!("template for project type {:?} is missing", self)
|
|
}
|
|
} else {
|
|
panic!("Rojo's templates were packed as a file instead of a directory")
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for InitKind {
|
|
type Err = anyhow::Error;
|
|
|
|
fn from_str(source: &str) -> Result<Self, Self::Err> {
|
|
match source {
|
|
"place" => Ok(InitKind::Place),
|
|
"model" => Ok(InitKind::Model),
|
|
"plugin" => Ok(InitKind::Plugin),
|
|
_ => Err(format_err!(
|
|
"Invalid init kind '{}'. Valid kinds are: place, model, plugin",
|
|
source
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Contains parameters used in templates to create a project.
|
|
struct ProjectParams {
|
|
name: String,
|
|
place_id: Option<u64>,
|
|
}
|
|
|
|
impl ProjectParams {
|
|
/// Render a template by replacing variables with project parameters.
|
|
fn render_template(&self, template: &str) -> String {
|
|
let place_id_str = self
|
|
.place_id
|
|
.map(|id| id.to_string())
|
|
.unwrap_or_else(|| "0".to_string());
|
|
|
|
template
|
|
.replace("{project_name}", &self.name)
|
|
.replace("{rojo_version}", env!("CARGO_PKG_VERSION"))
|
|
.replace("{place_id}", &place_id_str)
|
|
}
|
|
}
|
|
|
|
/// Tells whether we should initialize a Git repository inside the given path.
|
|
///
|
|
/// Will return false if the user doesn't have Git installed or if the path is
|
|
/// already inside a Git repository.
|
|
fn should_git_init(path: &Path) -> bool {
|
|
let result = Command::new("git")
|
|
.args(["rev-parse", "--is-inside-work-tree"])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.current_dir(path)
|
|
.status();
|
|
|
|
match result {
|
|
// If the command ran, but returned a non-zero exit code, we are not in
|
|
// a Git repo and we should initialize one.
|
|
Ok(status) => !status.success(),
|
|
|
|
// If the command failed to run, we probably don't have Git installed.
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Write a file if it does not exist yet, otherwise, leave it alone.
|
|
fn write_if_not_exists(path: &Path, contents: &str) -> Result<(), anyhow::Error> {
|
|
let file_res = OpenOptions::new().write(true).create_new(true).open(path);
|
|
|
|
let mut file = match file_res {
|
|
Ok(file) => file,
|
|
Err(err) => {
|
|
return match err.kind() {
|
|
io::ErrorKind::AlreadyExists => return Ok(()),
|
|
_ => Err(err.into()),
|
|
}
|
|
}
|
|
};
|
|
|
|
file.write_all(contents.as_bytes())?;
|
|
|
|
Ok(())
|
|
}
|