mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Enhance MCP functionality and improve syncback performance
- Expanded the TODO list with new tasks related to MCP modes, including standard, manual, fastfail, and dryrun options for better user control during sync operations. - Introduced a new `get_script` command to allow agents to read scripts directly from Roblox Studio, facilitating conflict resolution during sync. - Implemented a `GitIndexCache` to optimize file writes by skipping unchanged files based on blob SHA1 comparisons, improving syncback efficiency. - Added logging for agent connections and commands to enhance traceability and debugging capabilities. These changes aim to improve the overall functionality and performance of the MCP integration and syncback process.
This commit is contained in:
parent
581aeaeffa
commit
99b0af36fd
7 changed files with 170 additions and 15 deletions
|
|
@ -1787,6 +1787,18 @@ impl JobThreadContext {
|
|||
|
||||
if let Some(ref write_path) = source_path {
|
||||
if let Some(Variant::String(value)) = changed_value {
|
||||
if let Ok(existing) = fs::read(write_path) {
|
||||
if existing == value.as_bytes() {
|
||||
log::debug!(
|
||||
"Two-way sync: Skipping unchanged Source for {}",
|
||||
self.display_path(write_path)
|
||||
);
|
||||
if patch_set.stage_ids.contains(&id) {
|
||||
pending_stage_paths.push(write_path.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
log::info!(
|
||||
"Two-way sync: Writing Source to {}",
|
||||
self.display_path(write_path)
|
||||
|
|
|
|||
|
|
@ -120,9 +120,10 @@ fn run_live_syncback(project_path: &Path, payload: SyncbackPayload) -> anyhow::R
|
|||
drop(dom_old);
|
||||
|
||||
log::info!("Writing to the file system...");
|
||||
let git_cache = crate::git::GitIndexCache::new(base_path);
|
||||
result
|
||||
.fs_snapshot
|
||||
.write_to_vfs_parallel(base_path, session_old.vfs())?;
|
||||
.write_to_vfs_parallel(base_path, session_old.vfs(), git_cache.as_ref())?;
|
||||
|
||||
log::info!(
|
||||
"Finished live syncback: wrote {} files/folders, removed {}.",
|
||||
|
|
|
|||
|
|
@ -233,6 +233,8 @@ impl SyncbackCommand {
|
|||
|
||||
log::info!("Writing to the file system...");
|
||||
|
||||
let git_cache = crate::git::GitIndexCache::new(base_path);
|
||||
|
||||
if self.sourcemap {
|
||||
let sourcemap_path = base_path.join("sourcemap.json");
|
||||
|
||||
|
|
@ -241,9 +243,11 @@ impl SyncbackCommand {
|
|||
// so it doesn't need to wait for files to be on disk.
|
||||
let (write_result, sourcemap_result) = std::thread::scope(|s| {
|
||||
let write_handle = s.spawn(|| {
|
||||
result
|
||||
.fs_snapshot
|
||||
.write_to_vfs_parallel(base_path, session_old.vfs())
|
||||
result.fs_snapshot.write_to_vfs_parallel(
|
||||
base_path,
|
||||
session_old.vfs(),
|
||||
git_cache.as_ref(),
|
||||
)
|
||||
});
|
||||
|
||||
let sourcemap_handle = s.spawn(|| {
|
||||
|
|
@ -268,9 +272,11 @@ impl SyncbackCommand {
|
|||
Err(e) => log::warn!("Could not generate sourcemap: {}", e),
|
||||
}
|
||||
} else {
|
||||
result
|
||||
.fs_snapshot
|
||||
.write_to_vfs_parallel(base_path, session_old.vfs())?;
|
||||
result.fs_snapshot.write_to_vfs_parallel(
|
||||
base_path,
|
||||
session_old.vfs(),
|
||||
git_cache.as_ref(),
|
||||
)?;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
|
|
|
|||
61
src/git.rs
61
src/git.rs
|
|
@ -383,6 +383,67 @@ pub fn compute_blob_sha1(content: &str) -> String {
|
|||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn compute_blob_sha1_bytes(content: &[u8]) -> String {
|
||||
let header = format!("blob {}\0", content.len());
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(header.as_bytes());
|
||||
hasher.update(content);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
/// Pre-computed git index blob hashes for fast content comparison.
|
||||
///
|
||||
/// During syncback, we can avoid writing files whose content matches what's
|
||||
/// already on disk. For files tracked in the git index, comparing the blob
|
||||
/// SHA1 of the new content against the index entry's hash avoids reading
|
||||
/// the file from disk entirely.
|
||||
pub struct GitIndexCache {
|
||||
entries: HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
impl GitIndexCache {
|
||||
pub fn new(project_root: &Path) -> Option<Self> {
|
||||
let repo = open_repo(project_root)?;
|
||||
let repo_root = repo.workdir()?.to_owned();
|
||||
let index = repo.open_index().ok()?;
|
||||
|
||||
let mut entries = HashMap::new();
|
||||
for entry in index.entries() {
|
||||
let path_bstr = entry.path(&index);
|
||||
let path_str = path_bstr.to_str().ok()?;
|
||||
let rel_path = PathBuf::from(path_str);
|
||||
let hash_hex = entry.id.to_hex().to_string();
|
||||
|
||||
let abs_path = repo_root.join(&rel_path);
|
||||
let project_rel = abs_path
|
||||
.strip_prefix(project_root)
|
||||
.unwrap_or(&rel_path)
|
||||
.to_path_buf();
|
||||
entries.insert(project_rel, hash_hex);
|
||||
}
|
||||
|
||||
log::debug!("GitIndexCache: loaded {} index entries", entries.len());
|
||||
Some(Self { entries })
|
||||
}
|
||||
|
||||
/// Returns `true` if the new content's blob SHA1 matches the git index
|
||||
/// entry for this path, meaning the file on disk almost certainly
|
||||
/// already has this content.
|
||||
pub fn file_matches_index(&self, rel_path: &Path, content: &[u8]) -> bool {
|
||||
let normalized = PathBuf::from(rel_path.to_string_lossy().replace('\\', "/"));
|
||||
let index_hash = match self
|
||||
.entries
|
||||
.get(&normalized)
|
||||
.or_else(|| self.entries.get(rel_path))
|
||||
{
|
||||
Some(h) => h,
|
||||
None => return false,
|
||||
};
|
||||
let content_hash = compute_blob_sha1_bytes(content);
|
||||
&content_hash == index_hash
|
||||
}
|
||||
}
|
||||
|
||||
pub fn git_add(repo_root: &Path, paths: &[PathBuf]) {
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ use std::{thread, time::Duration};
|
|||
use memofs::Vfs;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::git::GitIndexCache;
|
||||
|
||||
/// Maximum number of retry attempts for filesystem operations on Windows.
|
||||
/// Windows can have transient "Access denied" errors due to antivirus scanning,
|
||||
/// filesystem timing, or file handle release delays.
|
||||
|
|
@ -274,16 +276,32 @@ impl FsSnapshot {
|
|||
/// Directory creation and removal remain sequential (ordering constraints),
|
||||
/// but file writes and file removals are parallelized using rayon.
|
||||
///
|
||||
/// Files whose content already matches what's on disk are skipped. When a
|
||||
/// `GitIndexCache` is provided, tracked files are checked via blob SHA1
|
||||
/// comparison (no disk read needed); all other files fall back to reading
|
||||
/// the existing content and comparing bytes.
|
||||
///
|
||||
/// This bypasses the VFS lock for file writes, using `std::fs` directly.
|
||||
/// This is safe because syncback uses a oneshot VFS with no caching or watching.
|
||||
pub fn write_to_vfs_parallel<P: AsRef<Path>>(&self, base: P, vfs: &Vfs) -> io::Result<()> {
|
||||
pub fn write_to_vfs_parallel<P: AsRef<Path>>(
|
||||
&self,
|
||||
base: P,
|
||||
vfs: &Vfs,
|
||||
git_cache: Option<&GitIndexCache>,
|
||||
) -> io::Result<()> {
|
||||
let base_path = base.as_ref();
|
||||
|
||||
// Phase 1: Create directories (sequential - parent must exist before child)
|
||||
let skipped_dirs = AtomicUsize::new(0);
|
||||
{
|
||||
let mut lock = vfs.lock();
|
||||
for dir_path in &self.added_dirs {
|
||||
match lock.create_dir_all(base_path.join(dir_path)) {
|
||||
let full = base_path.join(dir_path);
|
||||
if full.is_dir() {
|
||||
skipped_dirs.fetch_add(1, Ordering::Relaxed);
|
||||
continue;
|
||||
}
|
||||
match lock.create_dir_all(full) {
|
||||
Ok(_) => (),
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
|
||||
Err(err) => return Err(err),
|
||||
|
|
@ -292,13 +310,28 @@ impl FsSnapshot {
|
|||
} // Release lock before parallel phase
|
||||
|
||||
// Phase 2: Write files (parallel - independent operations)
|
||||
// On Windows, use retry logic for transient "Access denied" errors that can
|
||||
// occur due to antivirus scanning or filesystem timing issues.
|
||||
let write_errors = AtomicUsize::new(0);
|
||||
let first_error: std::sync::Mutex<Option<io::Error>> = std::sync::Mutex::new(None);
|
||||
let skipped_files = AtomicUsize::new(0);
|
||||
|
||||
self.added_files.par_iter().for_each(|(path, contents)| {
|
||||
let full_path = base_path.join(path);
|
||||
|
||||
if let Some(cache) = git_cache {
|
||||
if cache.file_matches_index(path, contents) {
|
||||
skipped_files.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match std::fs::read(&full_path) {
|
||||
Ok(existing) if existing == *contents => {
|
||||
skipped_files.fetch_add(1, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Err(err) = write_with_retry(&full_path, contents) {
|
||||
write_errors.fetch_add(1, Ordering::Relaxed);
|
||||
let mut guard = first_error.lock().unwrap();
|
||||
|
|
@ -369,10 +402,17 @@ impl FsSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Wrote {} directories and {} files to the file system (parallel)",
|
||||
self.added_dirs.len(),
|
||||
self.added_files.len()
|
||||
let skipped_file_count = skipped_files.load(Ordering::Relaxed);
|
||||
let skipped_dir_count = skipped_dirs.load(Ordering::Relaxed);
|
||||
let written_files = self.added_files.len() - skipped_file_count;
|
||||
let created_dirs = self.added_dirs.len() - skipped_dir_count;
|
||||
|
||||
log::info!(
|
||||
"Wrote {} files and {} directories ({} files and {} directories unchanged, skipped)",
|
||||
written_files,
|
||||
created_dirs,
|
||||
skipped_file_count,
|
||||
skipped_dir_count,
|
||||
);
|
||||
|
||||
let files_inside_dirs = self.removed_files.len() - files_to_remove.len();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue