Update dependencies and refactor git integration

- Added new dependencies in `Cargo.lock` and updated `Cargo.toml` to include `gix` for improved git functionality.
- Refactored git-related functions to utilize the `gix` library, enhancing performance and reliability in handling git operations.
- Simplified the process of initializing git repositories and committing changes within the CLI commands.
- Updated tests to reflect changes in git integration and ensure proper functionality.

These changes aim to modernize the git handling in the project and improve overall code quality.
This commit is contained in:
Joe 2026-03-01 07:49:03 -06:00
parent d8d6a53fce
commit 0a359ccaf1
12 changed files with 1954 additions and 473 deletions

1188
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -107,6 +107,7 @@ data-encoding = "2.10.0"
pathdiff = "0.2.3"
blake3 = "1.8.3"
gix = { version = "0.80", default-features = true, features = ["blocking-network-client"] }
sha1 = "0.10"
indexmap = { version = "2.13.0", features = ["serde"] }
rmp-serde = "1.3.1"

View file

@ -5,15 +5,6 @@ get the schemas for everything into cursor somehow
setup nightly build
suffix version for beta
[mcp]
* atlas_sync
- Basically equiv to git fast-forward merge
- Invokes Atlas sync from Roblox
- If there are NO unselected items in the tree (e.g. all were automatically selected), it just automatically hits accept
- Does nothing if we are running in connected-mode (already syncing)
- Agent should await the completion of this sync before proceeding
- If we are in connected-mode we will need to figure out a way to await syncing to finish to ensure state is good
[merging]
we should make the property diffs have arrows next to eachother so you can like "undo" certain things
need proper merging support in this UI.
@ -336,10 +327,24 @@ because have already loaded it, we can detect this.
this is an optimization to avoid writing 100,000 files or something on larger projects when file bytes match perfectly, or directories verifiably already exist.
make sure this is applied to plugin-based syncback, too.
[git]
I notice we actually use the git cli in a lot of places. is there a git library we can use instead?
[mcp]
* atlas_sync
- Basically equiv to git fast-forward merge
- Atlas plugin automatically connects (tries every 5s if nothing there, do not spam errors) via https://create.roblox.com/docs/reference/engine/classes/HttpService#CreateWebStreamClient
- Atlas serve automatically hosts a http based mcp server (NOT stdio based!)
- Decide whether it's easier to make a separate server from the api.rs file, or integrate it - integrating would be cleaner, but im not sure if it would support without a massive amount of changes
- When an agent invokes atlas_sync, it invokes Atlas sync from Roblox in the plugin as if the user clicked the sync button
- Must be on a mutex, so multiple agents cannot invoke it at the same time. Just refuses if it's currently happening
- Also it needs to refuse it if we're actively connected on the regular api layer too, not just the mcp based command.
- If we are connected already, respond to the agent with a proper notice. Make sure if we are in connected-mode where all things are syncing automatically all the time, we tell the agent. We need to tell the agent the configuration of the lua plugin probably?
- If we are in connected-mode we will need to figure out a way to await syncing to finish to ensure state is good?
- Upon the plugin connecting, if there are NO changes (Always/Initial) it should just immediately accept
- If all changes are pre-selected, e.g. from our git pre selection system, then we "fast-forward" by clicking accept immediately.
- If there are actually changes, the agent should hang while waiting for the user to either accept/deny them
- The output of the mcp request needs to conclude that it was a success and what changes were synced AND if it was rejected + what changes were presented to the user to sync.
- Let's just give a list of file paths on the actual disk (relative to project so like ReplicatedStorage/SomeFile.luau or ReplicatedStorage/SomeModel.json5 etc) ?
- Needs to include the DIRECTION, too.
- I'm not sure what is the most value on this, but we absolutely need to give the agent a response on what was actually synced, ideally what the content was(?), and the directions of sync.
- Does nothing if we are running in connected-mode (already syncing)
- Agent should await the completion of this sync before proceeding, this mcp command needs to yield and give the response as described above.
[cli]
make console tabbing support work
when i say console tabbing, like i can type "git c" and it will recommend "git checkout"
is there a rust crate to accomplish this? can we enable it on our current argument parser library being used?

View file

@ -5,6 +5,7 @@ expression: normalized_stdout
Writing default.project.json5
Writing src/adjacent.luau
Writing src/adjacent.meta.json5
Writing src/init/init.meta.json5
Writing src/init_/init.meta.json5
Writing src/model.model.json5
Writing src/init
Writing src/init_
Removing src/init

View file

@ -0,0 +1,10 @@
---
source: tests/tests/syncback.rs
expression: src/init_/init.meta.json5
---
{
attributes: {
trigger_reserialization: true
},
name: "init"
}

View file

@ -1,5 +1,4 @@
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{bail, Context};
use clap::Parser;
@ -95,25 +94,8 @@ impl CloneCommand {
syncback.run(global)?;
// Commit syncback result
if !skip_git {
log::info!("Staging syncback changes...");
let _ = Command::new("git")
.args(["add", "."])
.current_dir(&path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
log::info!("Committing syncback changes...");
let _ = Command::new("git")
.args(["commit", "--no-verify", "-m", "syncback"])
.current_dir(&path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
crate::git::git_add_all_and_commit(&path, "syncback");
}
if !self.skip_cd {

View file

@ -1,5 +1,6 @@
use std::process::{Command, Stdio};
use std::num::NonZero;
use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::{
collections::VecDeque,
path::{Path, PathBuf},
@ -24,7 +25,7 @@ static TEMPLATE_BINCODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/templ
/// 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`.
/// project directory. 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.
@ -57,7 +58,6 @@ impl InitCommand {
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 {
@ -122,31 +122,14 @@ impl InitCommand {
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()?;
let mut repo = gix::init(&base_path).context("Failed to initialize git repository")?;
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();
{
let mut config = repo.config_snapshot_mut();
let _ = config.set_raw_value(&gix::config::tree::Core::AUTO_CRLF, "false");
let _ = config.set_raw_value(&gix::config::tree::Core::EOL, "lf");
let _ = config.set_raw_value(&gix::config::tree::Core::SAFE_CRLF, "false");
let _ = config.commit_auto_rollback();
}
true
@ -158,54 +141,37 @@ impl InitCommand {
log::debug!("Cloning cursor rules...");
let cursor_dir = base_path.join(".cursor");
let result = Command::new("git")
.args([
"clone",
"--depth",
"1",
let result = (|| -> anyhow::Result<()> {
let prep = gix::prepare_clone(
"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();
&cursor_dir,
)?;
let mut prep = prep.with_shallow(gix::remote::fetch::Shallow::DepthAtRemote(
NonZero::new(1).unwrap(),
));
let (mut checkout, _) =
prep.fetch_then_checkout(gix::progress::Discard, &AtomicBool::new(false))?;
let (_repo, _) =
checkout.main_worktree(gix::progress::Discard, &AtomicBool::new(false))?;
Ok(())
})();
match result {
Ok(status) if status.success() => {
// Remove .git so it's just files, not a submodule
Ok(()) => {
let git_dir = cursor_dir.join(".git");
if git_dir.exists() {
let _ = fs::remove_dir_all(&git_dir);
}
println!("Cloned cursor rules successfully.");
}
_ => {
Err(_) => {
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();
crate::git::git_add_all_and_commit(&base_path, "Initial commit");
}
println!("Created project successfully.");
@ -219,6 +185,13 @@ impl InitCommand {
}
}
/// Tells whether we should initialize a Git repository inside the given path.
///
/// Returns true if the path is not already inside a Git repository.
fn should_git_init(path: &Path) -> bool {
gix::discover(path).is_err()
}
/// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)]
pub enum InitKind {
@ -275,14 +248,12 @@ impl FromStr for InitKind {
}
}
/// 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
@ -296,28 +267,6 @@ impl ProjectParams {
}
}
/// 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);

1002
src/git.rs

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,7 @@ mod web;
// TODO: Work out what we should expose publicly
pub use git::{git_add, git_add_all_and_commit};
pub use project::*;
pub use rojo_ref::*;
pub use serve_session::TreeFreshnessReport;

View file

@ -203,21 +203,17 @@ impl TestServeSession {
.expect("Couldn't copy project to temporary directory");
// Initialize git repo and run setup callback
Command::new("git")
.args(["init"])
.current_dir(&project_path)
.output()
.expect("git init failed");
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(&project_path)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(&project_path)
.output()
.unwrap();
let mut repo = gix::init(&project_path).expect("gix init failed");
{
let mut config = repo.config_snapshot_mut();
config
.set_raw_value(&gix::config::tree::User::NAME, "Test")
.unwrap();
config
.set_raw_value(&gix::config::tree::User::EMAIL, "test@test.com")
.unwrap();
let _ = config.commit_auto_rollback().unwrap();
}
setup(&project_path);

View file

@ -12,7 +12,7 @@
//! - Non-script changes are in changedIds but not in scriptCommittedHashes
use std::collections::HashMap;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use std::{fs, thread};
@ -24,27 +24,15 @@ use rbx_dom_weak::{ustr, UstrMap};
use crate::rojo_test::serve_util::TestServeSession;
fn git_commit_all(dir: &Path, msg: &str) {
Command::new("git")
.args(["add", "-A"])
.current_dir(dir)
.output()
.expect("git add failed");
Command::new("git")
.args(["commit", "-m", msg])
.current_dir(dir)
.output()
.expect("git commit failed");
librojo::git::git_add_all_and_commit(dir, msg);
}
fn git_stage(dir: &Path, file: &str) {
Command::new("git")
.args(["add", file])
.current_dir(dir)
.output()
.expect("git stage failed");
librojo::git_add(dir, &[PathBuf::from(file)]);
}
fn git_is_staged(dir: &Path, file: &str) -> bool {
use std::process::Command;
let output = Command::new("git")
.args(["diff", "--cached", "--name-only"])
.current_dir(dir)

View file

@ -93,7 +93,7 @@ syncback_tests! {
// do not leave any trace of themselves (new files get .json5)
ref_properties_pruned => ["src/Pointer1.model.json5", "src/Pointer2.model.json5", "src/Pointer3.model.json5"],
// Ensures that the `$schema` field roundtrips with syncback
schema_roundtrip => ["default.project.json5", "src/model.model.json5", "src/init/init.meta.json5", "src/adjacent.meta.json5"],
schema_roundtrip => ["default.project.json5", "src/model.model.json5", "src/init_/init.meta.json5", "src/adjacent.meta.json5"],
// Ensures that StringValues inside project files are written to the
// project file, but only if they don't have `$path` set
string_value_project => ["default.project.json5"],