diff --git a/TODO.txt b/TODO.txt index feada217..336669e8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -299,6 +299,31 @@ publish as a crate https://crates.io/crates/rojo [INFO librojo::change_processor] VFS event applied: no changes [INFO librojo::change_processor] Tree reconciliation: corrected 4 added, 4 removed (12.3s) + INFO [Studio] [Rojo] User selections: 0 push, 0 pull, 0 ignored + INFO [Studio] [Rojo] One-shot sync: disconnecting after initial sync complete +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~3/Meshes_Tacos_Cube4' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/Workspace/Events/FatSammy/Model/SammyArms/Arm~3/Meshes_SpyderSammyArms_spydersammy.007' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~3/Meshes_Tacos_Cube4' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/Workspace/Events/FatSammy/Model/SammyArms/Arm/Meshes_SpyderSammyArms_spydersammy.005' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~2/Meshes_Tacos_Cube3' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~2/Meshes_Tacos_Cube3' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/Workspace/Events/FatSammy/Model/SammyArms/Arm~2/Meshes_SpyderSammyArms_spydersammy.006' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~4/Meshes_Tacos_Cube5' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/Workspace/Events/FatSammy/Model/SammyArms/Arm~4/Meshes_SpyderSammyArms_spydersammy.008' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~4/Meshes_Tacos_Cube5' - instance not found in tree +DEBUG Could not resolve path reference Part1 at path '@game/ServerStorage/Backlog/Fruttina Jetolina/Fruttina Jetolina/Halo' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco/Meshes_Tacos_Cube1' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~4/Meshes_Tacos_Cube5' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco/Meshes_Tacos_Cube1' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~3/Meshes_Tacos_Cube4' - instance not found in tree +DEBUG Could not resolve path reference Part1 at path '@game/ServerStorage/Backlog/Crabo Biscotto/Crabo Biscotto/Halo' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco/Meshes_Tacos_Cube1' - instance not found in tree +DEBUG Could not resolve path reference Part0 at path '@game/ServerScriptService/Services/EventService/Events/Sammy's Base/SammyBase/Computer Desk/Taco~2/Meshes_Tacos_Cube3' - instance not found in tree + INFO Tree reconciliation: corrected 12 added, 12 removed (54.6s) + + DEBUG Loading project file from \\?\D:\UserGenerated\steal-a-brainrot\default.project.json5 + INFO Tree reconciliation: corrected 4 added, 4 removed (45.3s) + CONSTANTLY happening everytime i modify a file: [INFO librojo::change_processor] Tree reconciliation: corrected 4 added, 4 removed (22.1s) @@ -307,6 +332,7 @@ We need to investigate Tree reconciliation, make sure it's parallel, etc. I'd really like to just reconcile specific paths that were modified - but i'm not sure if that just defeats the purpose.. need to explore this topic later. Worst case - we reconcile only on connect, but do so in mega-parallel +[vscode file watcher] Also, we need to explore how VSCode deals with this. They seem to have ZERO issues. Maybe we can have a mode to tie into the vscode file watcher ourselves? luau-lsp does this. @@ -422,3 +448,23 @@ additional inspo: https://cursor.com/dashboard?tab=bugbot [git] complete audit of our git module to see what we can leverage the gix api directly instead of hard coding in really slow ways ourselves we can probably just call built-in functions to accomplish a lot of that hardcore tasks we are doing that would give 10-100x speedups + +[path canonicalize] +DEBUG Loading project file from D:\UserGenerated\steal-a-brainrot\default.project.json5 +DEBUG Prefetch walk: 248397 files + 74573 dirs from 1 root(s) in 2.2s +DEBUG Prefetch I/O: read 248397 files in 702.5ms, canonicalize 322970 paths in 3.7s +DEBUG Prefetch maps: 322970 is_file + 74573 children dirs built +DEBUG Prefetch total: 248397 files + 322970 canonical paths in 7.9s +get rid of this bulk path canonaclize where possible, it seems utterly pointless in most cases + +[git really slow] +DEBUG [TIMING] compute_git_metadata: open_repo 1ms +DEBUG [TIMING] compute_git_metadata: using 1 project prefixes +DEBUG [TIMING] compute_git_metadata: status iterator created 155ms +DEBUG [TIMING] compute_git_metadata: status iteration 2191ms (3 tracked, 0 untracked) +DEBUG [TIMING] compute_git_metadata: git_changed_files_impl 2347ms (3 tracked, 0 untracked) +DEBUG [TIMING] compute_git_metadata: tree resolution 0ms (3 changed -> 3 resolved) +DEBUG [TIMING] compute_git_metadata: batch_check_hashes 389ms (9 refs) +DEBUG [TIMING] compute_git_metadata: total 2739ms (3 changed_ids, 3 script_hashes, 0 new_file_ids) + INFO [Studio] [Rojo] One-shot mode: skipping all auto-accept paths, requiring explicit confirmation + diff --git a/src/change_processor.rs b/src/change_processor.rs index 94531e32..93aba74a 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -23,6 +23,12 @@ use crate::{ }, }; +/// Set to `true` to enable periodic tree reconciliation after VFS events. +/// Currently disabled: the per-event VFS handler handles file changes correctly, +/// and the reconciler has a persistent drift bug that causes an infinite +/// 12-14s re-snapshot loop on large projects. +const ENABLE_TREE_RECONCILIATION: bool = false; + /// Wrapper that displays a path relative to a project root directory. struct RelPath<'a> { path: &'a Path, @@ -156,18 +162,20 @@ impl ChangeProcessor { } } - // Schedule a reconciliation 200ms from now if one isn't pending. - if reconcile_at.is_none() { - reconcile_at = Some(Instant::now() + Duration::from_millis(200)); - } + if ENABLE_TREE_RECONCILIATION { + // Schedule a reconciliation 200ms from now if one isn't pending. + if reconcile_at.is_none() { + reconcile_at = Some(Instant::now() + Duration::from_millis(200)); + } - // If the deadline has passed, reconcile now. This check runs - // inside the VFS branch because during sustained event bursts, - // the `default` branch never fires (the VFS channel is never - // idle long enough). - if reconcile_at.is_some_and(|d| Instant::now() >= d) { - task.reconcile_tree(); - reconcile_at = None; + // If the deadline has passed, reconcile now. This check runs + // inside the VFS branch because during sustained event bursts, + // the `default` branch never fires (the VFS channel is never + // idle long enough). + if reconcile_at.is_some_and(|d| Instant::now() >= d) { + task.reconcile_tree(); + reconcile_at = None; + } } }, recv(tree_mutation_receiver) -> patch_set => { @@ -175,12 +183,19 @@ impl ChangeProcessor { }, recv(critical_error_receiver) -> err => { if let Ok(memofs::WatcherCriticalError::RescanRequired) = err { - log::warn!( - "VFS watcher lost events (RescanRequired). \ - Triggering full tree reconciliation." - ); - task.reconcile_tree(); - reconcile_at = None; + if ENABLE_TREE_RECONCILIATION { + log::warn!( + "VFS watcher lost events (RescanRequired). \ + Triggering full tree reconciliation." + ); + task.reconcile_tree(); + reconcile_at = None; + } else { + log::warn!( + "VFS watcher lost events (RescanRequired). \ + Tree reconciliation is disabled; some changes may be missed." + ); + } } }, recv(shutdown_receiver) -> _ => { @@ -190,10 +205,12 @@ impl ChangeProcessor { default(timeout) => { task.process_pending_recoveries(); - // If a reconciliation deadline has passed, run it now. - if reconcile_at.is_some_and(|d| Instant::now() >= d) { - task.reconcile_tree(); - reconcile_at = None; + if ENABLE_TREE_RECONCILIATION { + // If a reconciliation deadline has passed, run it now. + if reconcile_at.is_some_and(|d| Instant::now() >= d) { + task.reconcile_tree(); + reconcile_at = None; + } } }, } diff --git a/src/cli/init.rs b/src/cli/init.rs index 41258685..65c2ddc6 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,6 +1,4 @@ -use std::num::NonZero; use std::str::FromStr; -use std::sync::atomic::AtomicBool; use std::{ collections::VecDeque, path::{Path, PathBuf}, @@ -119,18 +117,10 @@ impl InitCommand { } } - let did_git_init = if !self.skip_git && should_git_init(&base_path) { + let did_git_init = if !self.skip_git && crate::git::git_repo_root(&base_path).is_none() { log::debug!("Initializing Git repository..."); - let mut repo = gix::init(&base_path).context("Failed to initialize git repository")?; - - { - 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(); - } + crate::git::git_init_repo(&base_path).context("Failed to initialize git repository")?; true } else { @@ -141,20 +131,10 @@ impl InitCommand { log::debug!("Cloning cursor rules..."); let cursor_dir = base_path.join(".cursor"); - let result = (|| -> anyhow::Result<()> { - let prep = gix::prepare_clone( - "https://github.com/jrmelsha/cursor-rules.git", - &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(()) - })(); + let result = crate::git::git_clone_shallow( + "https://github.com/jrmelsha/cursor-rules.git", + &cursor_dir, + ); match result { Ok(()) => { @@ -185,13 +165,6 @@ 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 { diff --git a/src/git.rs b/src/git.rs index 0567b463..7d0d82f0 100644 --- a/src/git.rs +++ b/src/git.rs @@ -5,6 +5,7 @@ use std::{ time::Instant, }; +use anyhow::Context; use gix::bstr::{BString, ByteSlice}; use rbx_dom_weak::types::Ref; use sha1::{Digest, Sha1}; @@ -18,21 +19,6 @@ fn open_repo(path: &Path) -> Option { gix::discover(path).ok() } -#[cfg(windows)] -fn strip_unc_prefix(path: &Path) -> PathBuf { - let s = path.to_string_lossy(); - if let Some(stripped) = s.strip_prefix(r"\\?\") { - PathBuf::from(stripped) - } else { - path.to_owned() - } -} - -#[cfg(not(windows))] -fn strip_unc_prefix(path: &Path) -> PathBuf { - path.to_owned() -} - pub fn git_repo_root(project_root: &Path) -> Option { let repo = open_repo(project_root)?; repo.workdir().map(|p| p.to_owned()) @@ -277,237 +263,125 @@ pub fn git_add(repo_root: &Path, paths: &[PathBuf]) { return; } - let repo = match open_repo(repo_root) { - Some(r) => r, - None => return, - }; - let mut index = match repo.open_index() { - Ok(i) => i, - Err(_) => return, - }; - - let work_dir = match repo.workdir() { - Some(d) => d.to_owned(), - None => return, - }; - - for path in paths { - let abs_path = if path.is_absolute() { - path.to_owned() - } else { - work_dir.join(path) - }; - let norm_abs = strip_unc_prefix(&abs_path); - let norm_work = strip_unc_prefix(&work_dir); - let rel_path = match norm_abs.strip_prefix(&norm_work) { - Ok(r) => r.to_owned(), - Err(_) => path.to_owned(), - }; - let content = match std::fs::read(&abs_path) { - Ok(c) => c, - Err(_) => continue, - }; - - let blob_id = match repo.write_blob(&content) { - Ok(id) => id.detach(), - Err(_) => continue, - }; - - let rel_str = rel_path.to_string_lossy().replace('\\', "/"); - let bstr_path: gix::bstr::BString = rel_str.into(); - - let gix_meta = match gix::index::fs::Metadata::from_path_no_follow(&abs_path) { - Ok(m) => m, - Err(_) => continue, - }; - let stat = gix::index::entry::Stat::from_fs(&gix_meta).unwrap_or_default(); - - if let Some(existing) = index - .entry_mut_by_path_and_stage(bstr_path.as_ref(), gix::index::entry::Stage::Unconflicted) - { - existing.id = blob_id; - existing.stat = stat; - } else { - index.dangerously_push_entry( - stat, - blob_id, - gix::index::entry::Flags::empty(), - gix::index::entry::Mode::FILE, - bstr_path.as_ref(), - ); - } - } - - index.sort_entries(); - if let Err(e) = index.write(gix::index::write::Options::default()) { - log::warn!("Failed to write git index: {}", e); - } -} - -/// Build a tree object from sorted index entries and write it to the ODB. -pub fn write_index_tree(repo: &gix::Repository, index: &gix::index::File) -> Option { - use gix::objs::tree; - use std::collections::BTreeMap; - - enum TreeNode { - Blob(gix::ObjectId, tree::EntryKind), - Tree(BTreeMap), - } - - let mut root: BTreeMap = BTreeMap::new(); - - for entry in index.entries() { - let path = entry.path(index).to_str_lossy(); - let parts: Vec<&str> = path.split('/').collect(); - - let mut current = &mut root; - for (i, part) in parts.iter().enumerate() { - if i == parts.len() - 1 { - let kind = if entry.mode == gix::index::entry::Mode::FILE_EXECUTABLE { - tree::EntryKind::BlobExecutable - } else { - tree::EntryKind::Blob - }; - current.insert(part.to_string(), TreeNode::Blob(entry.id, kind)); - } else { - current = match current - .entry(part.to_string()) - .or_insert_with(|| TreeNode::Tree(BTreeMap::new())) - { - TreeNode::Tree(ref mut map) => map, - _ => return None, - }; - } - } - } - - fn write_recursive( - repo: &gix::Repository, - nodes: &BTreeMap, - ) -> Option { - let mut tree = gix::objs::Tree::empty(); - for (name, node) in nodes { - match node { - TreeNode::Blob(id, kind) => { - tree.entries.push(tree::Entry { - mode: (*kind).into(), - filename: name.as_str().into(), - oid: *id, - }); - } - TreeNode::Tree(children) => { - let subtree_id = write_recursive(repo, children)?; - tree.entries.push(tree::Entry { - mode: tree::EntryKind::Tree.into(), - filename: name.as_str().into(), - oid: subtree_id, - }); - } - } - } - repo.write_object(&tree).ok().map(|id| id.detach()) - } - - write_recursive(repo, &root) -} - -/// Stage all worktree files and create a commit. -pub fn git_add_all_and_commit(dir: &Path, message: &str) { - let repo = match open_repo(dir) { - Some(r) => r, - None => return, - }; - let work_dir = match repo.workdir() { - Some(d) => d.to_owned(), - None => return, - }; - - log::info!("Staging all files..."); - let mut index = gix::index::File::from_state( - gix::index::State::new(repo.object_hash()), - repo.index_path(), - ); - - fn add_dir(repo: &gix::Repository, base: &Path, dir: &Path, index: &mut gix::index::File) { - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; - for entry in entries { - let entry = match entry { - Ok(e) => e, - Err(_) => continue, - }; - let path = entry.path(); - let name = match path.file_name().and_then(|n| n.to_str()) { - Some(n) => n, - None => continue, - }; - if name == ".git" { - continue; - } - if let Ok(ft) = entry.file_type() { - if ft.is_dir() { - add_dir(repo, base, &path, index); - } else if ft.is_file() { - let rel = match path.strip_prefix(base) { - Ok(r) => r, - Err(_) => continue, - }; - let rel_str = rel.to_string_lossy().replace('\\', "/"); - let content = match std::fs::read(&path) { - Ok(c) => c, - Err(_) => continue, - }; - let blob_id = match repo.write_blob(&content) { - Ok(id) => id.detach(), - Err(_) => continue, - }; - let gix_meta = match gix::index::fs::Metadata::from_path_no_follow(&path) { - Ok(m) => m, - Err(_) => continue, - }; - let stat = gix::index::entry::Stat::from_fs(&gix_meta).unwrap_or_default(); - let bstr_path: &gix::bstr::BStr = rel_str.as_bytes().as_bstr(); - index.dangerously_push_entry( - stat, - blob_id, - gix::index::entry::Flags::empty(), - gix::index::entry::Mode::FILE, - bstr_path, - ); - } - } - } - } - - add_dir(&repo, &work_dir, &work_dir, &mut index); - index.sort_entries(); - if index.write(gix::index::write::Options::default()).is_err() { - log::warn!("Failed to write git index"); - return; - } - - log::info!("Committing: {}", message); - let tree_id = match write_index_tree(&repo, &index) { - Some(id) => id, - None => { - log::warn!("Failed to write tree from index"); + let output = match std::process::Command::new("git") + .arg("add") + .arg("--") + .args(paths) + .current_dir(repo_root) + .output() + { + Ok(o) => o, + Err(e) => { + log::warn!("Failed to run git add: {}", e); return; } }; - let parents: Vec = repo - .head_id() - .ok() - .map(|id| id.detach()) - .into_iter() - .collect(); - if let Err(e) = repo.commit("HEAD", message, tree_id, parents.iter().copied()) { - log::warn!("Failed to commit: {}", e); + if !output.status.success() { + log::warn!( + "git add failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } } +/// Returns true if the given file path is staged in the git index. +pub fn git_is_staged(repo_root: &Path, file: &str) -> bool { + let output = match std::process::Command::new("git") + .args(["diff", "--cached", "--name-only"]) + .current_dir(repo_root) + .output() + { + Ok(o) => o, + Err(_) => return false, + }; + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().any(|l| l.trim() == file) +} + +/// Stage all worktree files and create a commit. +pub fn git_add_all_and_commit(dir: &Path, message: &str) { + let add = std::process::Command::new("git") + .args(["add", "-A"]) + .current_dir(dir) + .output(); + match add { + Ok(o) if !o.status.success() => { + log::warn!("git add -A failed: {}", String::from_utf8_lossy(&o.stderr)); + return; + } + Err(e) => { + log::warn!("Failed to run git add: {}", e); + return; + } + _ => {} + } + + let commit = std::process::Command::new("git") + .args(["commit", "-m", message]) + .current_dir(dir) + .output(); + match commit { + Ok(o) if !o.status.success() => { + log::warn!("git commit failed: {}", String::from_utf8_lossy(&o.stderr)); + } + Err(e) => { + log::warn!("Failed to run git commit: {}", e); + } + _ => { + log::info!("Committed: {}", message); + } + } +} + +/// Initialize a new git repository with line-ending config for cross-platform consistency. +pub fn git_init_repo(dir: &Path) -> anyhow::Result<()> { + let init = std::process::Command::new("git") + .arg("init") + .current_dir(dir) + .output() + .context("Failed to run git init")?; + if !init.status.success() { + anyhow::bail!("git init failed: {}", String::from_utf8_lossy(&init.stderr)); + } + + let config = std::process::Command::new("git") + .args(["config", "--local", "core.autocrlf", "false"]) + .current_dir(dir) + .output(); + if let Ok(o) = &config { + if !o.status.success() { + log::warn!("Failed to set core.autocrlf"); + } + } + let _ = std::process::Command::new("git") + .args(["config", "--local", "core.eol", "lf"]) + .current_dir(dir) + .output(); + let _ = std::process::Command::new("git") + .args(["config", "--local", "core.safecrlf", "false"]) + .current_dir(dir) + .output(); + + Ok(()) +} + +/// Shallow-clone a git repository into `target_dir`, depth 1. +pub fn git_clone_shallow(url: &str, target_dir: &Path) -> anyhow::Result<()> { + let output = std::process::Command::new("git") + .args(["clone", "--depth", "1", url]) + .arg(target_dir) + .output() + .context("Failed to run git clone")?; + if !output.status.success() { + anyhow::bail!( + "git clone failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(()) +} + struct ResolvedInstance { id: Ref, class_name: String, @@ -727,50 +601,26 @@ pub fn compute_git_metadata( /// This is useful because syncback may rewrite files with identical content, /// which can cause git to report them as modified due to timestamp changes. pub fn refresh_git_index(project_dir: &Path) { - let repo = match open_repo(project_dir) { - Some(r) => r, - None => { - log::debug!("Not a git repository, skipping index refresh."); + log::info!("Refreshing git index..."); + let output = match std::process::Command::new("git") + .args(["update-index", "--refresh"]) + .current_dir(project_dir) + .output() + { + Ok(o) => o, + Err(e) => { + log::debug!("Failed to run git update-index --refresh: {}", e); return; } }; - log::info!("Refreshing git index..."); - match repo.open_index() { - Ok(mut index) => { - let work_dir = match repo.workdir() { - Some(d) => d.to_owned(), - None => return, - }; - - let paths_and_stats: Vec<_> = index - .entries() - .iter() - .filter_map(|entry| { - let path_str = entry.path(&index).to_str_lossy(); - let full_path = work_dir.join(path_str.as_ref()); - let gix_meta = - gix::index::fs::Metadata::from_path_no_follow(&full_path).ok()?; - let stat = gix::index::entry::Stat::from_fs(&gix_meta).ok()?; - Some((path_str.to_string(), stat)) - }) - .collect(); - - for (path_str, stat) in paths_and_stats { - let bstr_path: &gix::bstr::BStr = path_str.as_bytes().as_bstr(); - if let Some(entry) = index - .entry_mut_by_path_and_stage(bstr_path, gix::index::entry::Stage::Unconflicted) - { - entry.stat = stat; - } - } - - match index.write(gix::index::write::Options::default()) { - Ok(_) => log::info!("Git index refreshed."), - Err(e) => log::warn!("Failed to write refreshed git index: {}", e), - } - } - Err(e) => log::warn!("Failed to open git index for refresh: {}", e), + if output.status.success() { + log::info!("Git index refreshed."); + } else { + log::warn!( + "git update-index --refresh failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } } @@ -781,7 +631,7 @@ mod tests { use tempfile::tempdir; fn git_init(dir: &Path) { - gix::init(dir).expect("gix init failed"); + git_init_repo(dir).expect("git init failed"); let config_path = dir.join(".git/config"); let mut content = fs::read_to_string(&config_path).unwrap_or_default(); content.push_str("[user]\n\tname = Test\n\temail = test@test.com\n"); @@ -853,20 +703,31 @@ mod tests { assert_ne!(h1, h2); } + fn git_hash_object(dir: &Path, content: &[u8]) -> String { + let output = std::process::Command::new("git") + .args(["hash-object", "--stdin"]) + .current_dir(dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .and_then(|mut child| { + use std::io::Write; + child.stdin.take().unwrap().write_all(content).unwrap(); + child.wait_with_output() + }) + .expect("git hash-object failed"); + String::from_utf8(output.stdout).unwrap().trim().to_string() + } + #[test] - fn blob_sha1_matches_gix_hash_object() { + fn blob_sha1_matches_git_hash_object() { let dir = tempdir().unwrap(); git_init(dir.path()); let content = "print('test script')\n"; - fs::write(dir.path().join("test.luau"), content).unwrap(); - - let repo = open_repo(dir.path()).unwrap(); - let blob_id = repo.write_blob(content.as_bytes()).unwrap(); - let gix_hash = blob_id.to_hex().to_string(); - + let git_hash = git_hash_object(dir.path(), content.as_bytes()); let our_hash = compute_blob_sha1(content); - assert_eq!(our_hash, gix_hash); + assert_eq!(our_hash, git_hash); } // ----------------------------------------------------------------------- @@ -1362,9 +1223,8 @@ mod tests { let dir = tempdir().unwrap(); git_init(dir.path()); - let repo = open_repo(dir.path()).unwrap(); - let gix_hash = repo.write_blob(b"").unwrap().to_hex().to_string(); - assert_eq!(compute_blob_sha1(""), gix_hash); + let git_hash = git_hash_object(dir.path(), b""); + assert_eq!(compute_blob_sha1(""), git_hash); } #[test] @@ -1373,13 +1233,8 @@ mod tests { let dir = tempdir().unwrap(); git_init(dir.path()); - let repo = open_repo(dir.path()).unwrap(); - let gix_hash = repo - .write_blob(content.as_bytes()) - .unwrap() - .to_hex() - .to_string(); - assert_eq!(compute_blob_sha1(content), gix_hash); + let git_hash = git_hash_object(dir.path(), content.as_bytes()); + assert_eq!(compute_blob_sha1(content), git_hash); } #[test] @@ -1390,12 +1245,7 @@ mod tests { let dir = tempdir().unwrap(); git_init(dir.path()); - let repo = open_repo(dir.path()).unwrap(); - let gix_hash = repo - .write_blob(content.as_bytes()) - .unwrap() - .to_hex() - .to_string(); - assert_eq!(compute_blob_sha1(&content), gix_hash); + let git_hash = git_hash_object(dir.path(), content.as_bytes()); + assert_eq!(compute_blob_sha1(&content), git_hash); } } diff --git a/src/lib.rs b/src/lib.rs index 72e4b7e2..b4f7e6ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ mod web; // TODO: Work out what we should expose publicly -pub use git::{git_add, git_add_all_and_commit}; +pub use git::{git_add, git_add_all_and_commit, git_clone_shallow, git_init_repo, git_is_staged}; pub use project::*; pub use rojo_ref::*; pub use serve_session::TreeFreshnessReport; diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index e1e606db..15fb53f5 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -202,11 +202,7 @@ impl TestServeSession { copy_recursive(&source_path, &project_path) .expect("Couldn't copy project to temporary directory"); - // Initialize git repo and persist config to disk so that - // git_add_all_and_commit (which opens a fresh gix handle via - // gix::discover) can read user identity. The gix SnapshotMut - // API is in-memory only and never writes to .git/config. - gix::init(&project_path).expect("gix init failed"); + librojo::git::git_init_repo(&project_path).expect("git init failed"); fs::write( project_path.join(".git/config"), "[user]\n\tname = Test\n\temail = test@test.com\n[core]\n\tautocrlf = false\n", diff --git a/tests/tests/git_sync_defaults.rs b/tests/tests/git_sync_defaults.rs index 3b797871..a3acee27 100644 --- a/tests/tests/git_sync_defaults.rs +++ b/tests/tests/git_sync_defaults.rs @@ -40,14 +40,7 @@ fn git_stage(dir: &Path, file: &str) { } 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) - .output() - .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - stdout.lines().any(|l| l.trim() == file) + librojo::git_is_staged(dir, file) } fn compute_blob_sha1(content: &str) -> String {