Implement tree reconciliation toggle and enhance git functionality

- Introduced a constant to enable or disable periodic tree reconciliation after VFS events, addressing a persistent drift bug that caused infinite re-snapshot loops on large projects.
- Updated the `ChangeProcessor` to conditionally execute tree reconciliation based on the new constant, improving performance during file modifications.
- Enhanced the git module by adding new functions for shallow cloning and checking if files are staged, streamlining git operations.
- Refactored the `InitCommand` to utilize the new git functions for repository initialization and cloning, improving clarity and reducing redundancy.

These changes aim to optimize file change handling and enhance git integration within the project.
This commit is contained in:
Joe 2026-03-01 18:16:03 -06:00
parent 1a03554ee8
commit 76b5d8bc75
7 changed files with 248 additions and 373 deletions

View file

@ -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] VFS event applied: no changes
[INFO librojo::change_processor] Tree reconciliation: corrected 4 added, 4 removed (12.3s) [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: CONSTANTLY happening everytime i modify a file:
[INFO librojo::change_processor] Tree reconciliation: corrected 4 added, 4 removed (22.1s) [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. 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 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. 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. 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] [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 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 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

View file

@ -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. /// Wrapper that displays a path relative to a project root directory.
struct RelPath<'a> { struct RelPath<'a> {
path: &'a Path, path: &'a Path,
@ -156,6 +162,7 @@ impl ChangeProcessor {
} }
} }
if ENABLE_TREE_RECONCILIATION {
// Schedule a reconciliation 200ms from now if one isn't pending. // Schedule a reconciliation 200ms from now if one isn't pending.
if reconcile_at.is_none() { if reconcile_at.is_none() {
reconcile_at = Some(Instant::now() + Duration::from_millis(200)); reconcile_at = Some(Instant::now() + Duration::from_millis(200));
@ -169,18 +176,26 @@ impl ChangeProcessor {
task.reconcile_tree(); task.reconcile_tree();
reconcile_at = None; reconcile_at = None;
} }
}
}, },
recv(tree_mutation_receiver) -> patch_set => { recv(tree_mutation_receiver) -> patch_set => {
task.handle_tree_event(patch_set?); task.handle_tree_event(patch_set?);
}, },
recv(critical_error_receiver) -> err => { recv(critical_error_receiver) -> err => {
if let Ok(memofs::WatcherCriticalError::RescanRequired) = err { if let Ok(memofs::WatcherCriticalError::RescanRequired) = err {
if ENABLE_TREE_RECONCILIATION {
log::warn!( log::warn!(
"VFS watcher lost events (RescanRequired). \ "VFS watcher lost events (RescanRequired). \
Triggering full tree reconciliation." Triggering full tree reconciliation."
); );
task.reconcile_tree(); task.reconcile_tree();
reconcile_at = None; reconcile_at = None;
} else {
log::warn!(
"VFS watcher lost events (RescanRequired). \
Tree reconciliation is disabled; some changes may be missed."
);
}
} }
}, },
recv(shutdown_receiver) -> _ => { recv(shutdown_receiver) -> _ => {
@ -190,11 +205,13 @@ impl ChangeProcessor {
default(timeout) => { default(timeout) => {
task.process_pending_recoveries(); task.process_pending_recoveries();
if ENABLE_TREE_RECONCILIATION {
// If a reconciliation deadline has passed, run it now. // If a reconciliation deadline has passed, run it now.
if reconcile_at.is_some_and(|d| Instant::now() >= d) { if reconcile_at.is_some_and(|d| Instant::now() >= d) {
task.reconcile_tree(); task.reconcile_tree();
reconcile_at = None; reconcile_at = None;
} }
}
}, },
} }
} }

View file

@ -1,6 +1,4 @@
use std::num::NonZero;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
path::{Path, PathBuf}, 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..."); log::debug!("Initializing Git repository...");
let mut repo = gix::init(&base_path).context("Failed to initialize git repository")?; crate::git::git_init_repo(&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();
}
true true
} else { } else {
@ -141,20 +131,10 @@ impl InitCommand {
log::debug!("Cloning cursor rules..."); log::debug!("Cloning cursor rules...");
let cursor_dir = base_path.join(".cursor"); let cursor_dir = base_path.join(".cursor");
let result = (|| -> anyhow::Result<()> { let result = crate::git::git_clone_shallow(
let prep = gix::prepare_clone(
"https://github.com/jrmelsha/cursor-rules.git", "https://github.com/jrmelsha/cursor-rules.git",
&cursor_dir, &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 { match result {
Ok(()) => { 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. /// The templates we support for initializing a Rojo project.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum InitKind { pub enum InitKind {

View file

@ -5,6 +5,7 @@ use std::{
time::Instant, time::Instant,
}; };
use anyhow::Context;
use gix::bstr::{BString, ByteSlice}; use gix::bstr::{BString, ByteSlice};
use rbx_dom_weak::types::Ref; use rbx_dom_weak::types::Ref;
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
@ -18,21 +19,6 @@ fn open_repo(path: &Path) -> Option<gix::Repository> {
gix::discover(path).ok() 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<PathBuf> { pub fn git_repo_root(project_root: &Path) -> Option<PathBuf> {
let repo = open_repo(project_root)?; let repo = open_repo(project_root)?;
repo.workdir().map(|p| p.to_owned()) repo.workdir().map(|p| p.to_owned())
@ -277,235 +263,123 @@ pub fn git_add(repo_root: &Path, paths: &[PathBuf]) {
return; return;
} }
let repo = match open_repo(repo_root) { let output = match std::process::Command::new("git")
Some(r) => r, .arg("add")
None => return, .arg("--")
}; .args(paths)
let mut index = match repo.open_index() { .current_dir(repo_root)
Ok(i) => i, .output()
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; Ok(o) => o,
existing.stat = stat; Err(e) => {
} else { log::warn!("Failed to run git add: {}", e);
index.dangerously_push_entry( return;
stat, }
blob_id, };
gix::index::entry::Flags::empty(),
gix::index::entry::Mode::FILE, if !output.status.success() {
bstr_path.as_ref(), log::warn!(
"git add failed: {}",
String::from_utf8_lossy(&output.stderr)
); );
} }
} }
index.sort_entries(); /// Returns true if the given file path is staged in the git index.
if let Err(e) = index.write(gix::index::write::Options::default()) { pub fn git_is_staged(repo_root: &Path, file: &str) -> bool {
log::warn!("Failed to write git index: {}", e); let output = match std::process::Command::new("git")
} .args(["diff", "--cached", "--name-only"])
} .current_dir(repo_root)
.output()
/// 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<gix::ObjectId> {
use gix::objs::tree;
use std::collections::BTreeMap;
enum TreeNode {
Blob(gix::ObjectId, tree::EntryKind),
Tree(BTreeMap<String, TreeNode>),
}
let mut root: BTreeMap<String, TreeNode> = 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, Ok(o) => o,
_ => return None, Err(_) => return false,
}; };
} let stdout = String::from_utf8_lossy(&output.stdout);
} stdout.lines().any(|l| l.trim() == file)
}
fn write_recursive(
repo: &gix::Repository,
nodes: &BTreeMap<String, TreeNode>,
) -> Option<gix::ObjectId> {
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. /// Stage all worktree files and create a commit.
pub fn git_add_all_and_commit(dir: &Path, message: &str) { pub fn git_add_all_and_commit(dir: &Path, message: &str) {
let repo = match open_repo(dir) { let add = std::process::Command::new("git")
Some(r) => r, .args(["add", "-A"])
None => return, .current_dir(dir)
}; .output();
let work_dir = match repo.workdir() { match add {
Some(d) => d.to_owned(), Ok(o) if !o.status.success() => {
None => return, log::warn!("git add -A failed: {}", String::from_utf8_lossy(&o.stderr));
};
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; return;
} }
Err(e) => {
log::info!("Committing: {}", message); log::warn!("Failed to run git add: {}", e);
let tree_id = match write_index_tree(&repo, &index) {
Some(id) => id,
None => {
log::warn!("Failed to write tree from index");
return; return;
} }
}; _ => {}
let parents: Vec<gix::ObjectId> = 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);
} }
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 { struct ResolvedInstance {
@ -727,50 +601,26 @@ pub fn compute_git_metadata(
/// This is useful because syncback may rewrite files with identical content, /// This is useful because syncback may rewrite files with identical content,
/// which can cause git to report them as modified due to timestamp changes. /// which can cause git to report them as modified due to timestamp changes.
pub fn refresh_git_index(project_dir: &Path) { pub fn refresh_git_index(project_dir: &Path) {
let repo = match open_repo(project_dir) { log::info!("Refreshing git index...");
Some(r) => r, let output = match std::process::Command::new("git")
None => { .args(["update-index", "--refresh"])
log::debug!("Not a git repository, skipping index refresh."); .current_dir(project_dir)
.output()
{
Ok(o) => o,
Err(e) => {
log::debug!("Failed to run git update-index --refresh: {}", e);
return; return;
} }
}; };
log::info!("Refreshing git index..."); if output.status.success() {
match repo.open_index() { log::info!("Git index refreshed.");
Ok(mut index) => { } else {
let work_dir = match repo.workdir() { log::warn!(
Some(d) => d.to_owned(), "git update-index --refresh failed: {}",
None => return, String::from_utf8_lossy(&output.stderr)
}; );
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),
} }
} }
@ -781,7 +631,7 @@ mod tests {
use tempfile::tempdir; use tempfile::tempdir;
fn git_init(dir: &Path) { 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 config_path = dir.join(".git/config");
let mut content = fs::read_to_string(&config_path).unwrap_or_default(); 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"); content.push_str("[user]\n\tname = Test\n\temail = test@test.com\n");
@ -853,20 +703,31 @@ mod tests {
assert_ne!(h1, h2); 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] #[test]
fn blob_sha1_matches_gix_hash_object() { fn blob_sha1_matches_git_hash_object() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
git_init(dir.path()); git_init(dir.path());
let content = "print('test script')\n"; let content = "print('test script')\n";
fs::write(dir.path().join("test.luau"), content).unwrap(); let git_hash = git_hash_object(dir.path(), content.as_bytes());
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 our_hash = compute_blob_sha1(content); 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(); let dir = tempdir().unwrap();
git_init(dir.path()); git_init(dir.path());
let repo = open_repo(dir.path()).unwrap(); let git_hash = git_hash_object(dir.path(), b"");
let gix_hash = repo.write_blob(b"").unwrap().to_hex().to_string(); assert_eq!(compute_blob_sha1(""), git_hash);
assert_eq!(compute_blob_sha1(""), gix_hash);
} }
#[test] #[test]
@ -1373,13 +1233,8 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
git_init(dir.path()); git_init(dir.path());
let repo = open_repo(dir.path()).unwrap(); let git_hash = git_hash_object(dir.path(), content.as_bytes());
let gix_hash = repo assert_eq!(compute_blob_sha1(content), git_hash);
.write_blob(content.as_bytes())
.unwrap()
.to_hex()
.to_string();
assert_eq!(compute_blob_sha1(content), gix_hash);
} }
#[test] #[test]
@ -1390,12 +1245,7 @@ mod tests {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
git_init(dir.path()); git_init(dir.path());
let repo = open_repo(dir.path()).unwrap(); let git_hash = git_hash_object(dir.path(), content.as_bytes());
let gix_hash = repo assert_eq!(compute_blob_sha1(&content), git_hash);
.write_blob(content.as_bytes())
.unwrap()
.to_hex()
.to_string();
assert_eq!(compute_blob_sha1(&content), gix_hash);
} }
} }

View file

@ -29,7 +29,7 @@ mod web;
// TODO: Work out what we should expose publicly // 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 project::*;
pub use rojo_ref::*; pub use rojo_ref::*;
pub use serve_session::TreeFreshnessReport; pub use serve_session::TreeFreshnessReport;

View file

@ -202,11 +202,7 @@ impl TestServeSession {
copy_recursive(&source_path, &project_path) copy_recursive(&source_path, &project_path)
.expect("Couldn't copy project to temporary directory"); .expect("Couldn't copy project to temporary directory");
// Initialize git repo and persist config to disk so that librojo::git::git_init_repo(&project_path).expect("git init failed");
// 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");
fs::write( fs::write(
project_path.join(".git/config"), project_path.join(".git/config"),
"[user]\n\tname = Test\n\temail = test@test.com\n[core]\n\tautocrlf = false\n", "[user]\n\tname = Test\n\temail = test@test.com\n[core]\n\tautocrlf = false\n",

View file

@ -40,14 +40,7 @@ fn git_stage(dir: &Path, file: &str) {
} }
fn git_is_staged(dir: &Path, file: &str) -> bool { fn git_is_staged(dir: &Path, file: &str) -> bool {
use std::process::Command; librojo::git_is_staged(dir, file)
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)
} }
fn compute_blob_sha1(content: &str) -> String { fn compute_blob_sha1(content: &str) -> String {