mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- Added a `validChildren` field to the `VCache` structure to track valid child instances during the matching process. - Refactored the `cacheVirtual` function to improve readability by formatting parameters across multiple lines. - Updated the `computePairCost` function to utilize the new `validChildren` field for more accurate cost calculations. - Simplified the `matchChildren` function by removing redundant code and improving the handling of matched instances. - Enhanced test cases for the `matchChildren` function to ensure consistent behavior and validate the new changes. These updates improve the efficiency and clarity of the matching process, ensuring better performance and maintainability.
765 lines
24 KiB
Rust
765 lines
24 KiB
Rust
use std::{
|
|
collections::{HashMap, HashSet},
|
|
path::{Path, PathBuf},
|
|
process::{Command, Stdio},
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use rbx_dom_weak::types::Ref;
|
|
use sha1::{Digest, Sha1};
|
|
|
|
use crate::{snapshot::RojoTree, web::interface::GitMetadata};
|
|
|
|
const SCRIPT_CLASSES: &[&str] = &["Script", "LocalScript", "ModuleScript"];
|
|
|
|
fn is_script_class(class: &str) -> bool {
|
|
SCRIPT_CLASSES.contains(&class)
|
|
}
|
|
|
|
pub fn git_repo_root(project_root: &Path) -> Option<PathBuf> {
|
|
let output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&project_root.to_string_lossy(),
|
|
"rev-parse",
|
|
"--show-toplevel",
|
|
])
|
|
.output()
|
|
.ok()?;
|
|
|
|
if !output.status.success() {
|
|
return None;
|
|
}
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let path_str = stdout.trim();
|
|
if path_str.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(PathBuf::from(path_str))
|
|
}
|
|
|
|
fn git_changed_files(repo_root: &Path) -> Option<HashSet<PathBuf>> {
|
|
let mut changed = HashSet::new();
|
|
|
|
let diff_output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&repo_root.to_string_lossy(),
|
|
"diff",
|
|
"HEAD",
|
|
"--name-only",
|
|
])
|
|
.output()
|
|
.ok()?;
|
|
|
|
if diff_output.status.success() {
|
|
for line in String::from_utf8_lossy(&diff_output.stdout).lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
changed.insert(PathBuf::from(trimmed));
|
|
}
|
|
}
|
|
} else {
|
|
log::debug!("git diff HEAD failed (possibly no commits yet), skipping diff");
|
|
}
|
|
|
|
let untracked_output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&repo_root.to_string_lossy(),
|
|
"ls-files",
|
|
"--others",
|
|
"--exclude-standard",
|
|
])
|
|
.output()
|
|
.ok()?;
|
|
|
|
if untracked_output.status.success() {
|
|
for line in String::from_utf8_lossy(&untracked_output.stdout).lines() {
|
|
let trimmed = line.trim();
|
|
if !trimmed.is_empty() {
|
|
changed.insert(PathBuf::from(trimmed));
|
|
}
|
|
}
|
|
}
|
|
|
|
if changed.is_empty() && !diff_output.status.success() {
|
|
return None;
|
|
}
|
|
|
|
Some(changed)
|
|
}
|
|
|
|
fn git_show(repo_root: &Path, object_ref: &str) -> Option<String> {
|
|
let output = Command::new("git")
|
|
.args(["-C", &repo_root.to_string_lossy(), "show", object_ref])
|
|
.output()
|
|
.ok()?;
|
|
|
|
if !output.status.success() {
|
|
return None;
|
|
}
|
|
|
|
String::from_utf8(output.stdout).ok()
|
|
}
|
|
|
|
fn git_show_head(repo_root: &Path, rel_path: &Path) -> Option<String> {
|
|
let object_ref = format!("HEAD:{}", rel_path.to_string_lossy().replace('\\', "/"));
|
|
git_show(repo_root, &object_ref)
|
|
}
|
|
|
|
fn git_show_staged(repo_root: &Path, rel_path: &Path) -> Option<String> {
|
|
let object_ref = format!(":0:{}", rel_path.to_string_lossy().replace('\\', "/"));
|
|
git_show(repo_root, &object_ref)
|
|
}
|
|
|
|
pub fn compute_blob_sha1(content: &str) -> String {
|
|
let header = format!("blob {}\0", content.len());
|
|
let mut hasher = Sha1::new();
|
|
hasher.update(header.as_bytes());
|
|
hasher.update(content.as_bytes());
|
|
format!("{:x}", hasher.finalize())
|
|
}
|
|
|
|
pub fn git_add(repo_root: &Path, paths: &[PathBuf]) {
|
|
if paths.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let mut cmd = Command::new("git");
|
|
cmd.args(["-C", &repo_root.to_string_lossy(), "add", "--"]);
|
|
for path in paths {
|
|
cmd.arg(path);
|
|
}
|
|
|
|
match cmd.output() {
|
|
Ok(output) => {
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
log::warn!("git add failed: {}", stderr.trim());
|
|
}
|
|
}
|
|
Err(err) => {
|
|
log::warn!("Failed to run git add: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ResolvedInstance {
|
|
id: Ref,
|
|
class_name: String,
|
|
rel_path: PathBuf,
|
|
}
|
|
|
|
/// Computes git metadata for the two-way sync confirmation UI.
|
|
///
|
|
/// `repo_root` must be the git repository root (from `git_repo_root()`),
|
|
/// cached at session start by `ServeSession`.
|
|
///
|
|
/// Uses a two-phase approach to avoid holding the tree lock during I/O:
|
|
/// 1. Run git commands to get changed files (no lock)
|
|
/// 2. Briefly lock tree to resolve file paths to instance Refs and class names
|
|
/// 3. Run git show for each changed script and compute hashes (no lock)
|
|
pub fn compute_git_metadata(tree_handle: &Arc<Mutex<RojoTree>>, repo_root: &Path) -> GitMetadata {
|
|
let changed_files = match git_changed_files(repo_root) {
|
|
Some(files) => files,
|
|
None => {
|
|
return GitMetadata {
|
|
changed_ids: Vec::new(),
|
|
script_committed_hashes: HashMap::new(),
|
|
};
|
|
}
|
|
};
|
|
|
|
if changed_files.is_empty() {
|
|
return GitMetadata {
|
|
changed_ids: Vec::new(),
|
|
script_committed_hashes: HashMap::new(),
|
|
};
|
|
}
|
|
|
|
// Brief lock: resolve changed file paths to instance Refs and class names
|
|
let resolved: Vec<ResolvedInstance> = {
|
|
let tree = tree_handle.lock().unwrap();
|
|
let mut result = Vec::new();
|
|
|
|
for rel_path in &changed_files {
|
|
let abs_path = repo_root.join(rel_path);
|
|
let canonical_path = std::fs::canonicalize(&abs_path).ok();
|
|
|
|
let first = tree.get_ids_at_path(&abs_path);
|
|
let ids = if !first.is_empty() {
|
|
first
|
|
} else if let Some(ref canon) = canonical_path {
|
|
tree.get_ids_at_path(canon)
|
|
} else {
|
|
first
|
|
};
|
|
|
|
for &id in ids {
|
|
if let Some(instance) = tree.get_instance(id) {
|
|
result.push(ResolvedInstance {
|
|
id,
|
|
class_name: instance.class_name().to_string(),
|
|
rel_path: rel_path.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}; // Lock released
|
|
|
|
let changed_ids: Vec<Ref> = resolved.iter().map(|ri| ri.id).collect();
|
|
let mut script_committed_hashes: HashMap<Ref, Vec<String>> = HashMap::new();
|
|
|
|
for ri in &resolved {
|
|
if !is_script_class(&ri.class_name) {
|
|
continue;
|
|
}
|
|
|
|
let mut hashes = Vec::with_capacity(2);
|
|
|
|
if let Some(head_content) = git_show_head(repo_root, &ri.rel_path) {
|
|
hashes.push(compute_blob_sha1(&head_content));
|
|
}
|
|
|
|
if let Some(staged_content) = git_show_staged(repo_root, &ri.rel_path) {
|
|
let staged_hash = compute_blob_sha1(&staged_content);
|
|
if hashes.is_empty() || hashes[0] != staged_hash {
|
|
hashes.push(staged_hash);
|
|
}
|
|
}
|
|
|
|
if !hashes.is_empty() {
|
|
script_committed_hashes.insert(ri.id, hashes);
|
|
}
|
|
}
|
|
|
|
GitMetadata {
|
|
changed_ids,
|
|
script_committed_hashes,
|
|
}
|
|
}
|
|
|
|
/// Refreshes the git index if the project is in a git repository.
|
|
///
|
|
/// 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 mut check_dir = Some(project_dir);
|
|
let mut is_git_repo = false;
|
|
while let Some(dir) = check_dir {
|
|
if dir.join(".git").exists() {
|
|
is_git_repo = true;
|
|
break;
|
|
}
|
|
check_dir = dir.parent();
|
|
}
|
|
|
|
if is_git_repo {
|
|
log::info!("Refreshing git index...");
|
|
match Command::new("git")
|
|
.args(["update-index", "--refresh", "-q"])
|
|
.current_dir(project_dir)
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
{
|
|
Ok(_) => log::info!("Git index refreshed."),
|
|
Err(e) => log::warn!("Failed to run git update-index --refresh: {}", e),
|
|
}
|
|
} else {
|
|
log::debug!("Not a git repository, skipping index refresh.");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use tempfile::tempdir;
|
|
|
|
fn git_init(dir: &Path) {
|
|
Command::new("git")
|
|
.args(["-C", &dir.to_string_lossy(), "init"])
|
|
.output()
|
|
.expect("git init failed");
|
|
Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&dir.to_string_lossy(),
|
|
"config",
|
|
"user.email",
|
|
"test@test.com",
|
|
])
|
|
.output()
|
|
.unwrap();
|
|
Command::new("git")
|
|
.args(["-C", &dir.to_string_lossy(), "config", "user.name", "Test"])
|
|
.output()
|
|
.unwrap();
|
|
}
|
|
|
|
fn git_commit_all(dir: &Path, msg: &str) {
|
|
Command::new("git")
|
|
.args(["-C", &dir.to_string_lossy(), "add", "-A"])
|
|
.output()
|
|
.unwrap();
|
|
Command::new("git")
|
|
.args(["-C", &dir.to_string_lossy(), "commit", "-m", msg])
|
|
.output()
|
|
.unwrap();
|
|
}
|
|
|
|
fn git_stage(dir: &Path, file: &str) {
|
|
Command::new("git")
|
|
.args(["-C", &dir.to_string_lossy(), "add", file])
|
|
.output()
|
|
.unwrap();
|
|
}
|
|
|
|
fn git_is_staged(dir: &Path, file: &str) -> bool {
|
|
let output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&dir.to_string_lossy(),
|
|
"diff",
|
|
"--cached",
|
|
"--name-only",
|
|
])
|
|
.output()
|
|
.unwrap();
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
stdout.lines().any(|l| l.trim() == file)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// compute_blob_sha1
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn blob_sha1_empty_content() {
|
|
let hash = compute_blob_sha1("");
|
|
// git hash-object -t blob --stdin <<< '' produces the hash for empty blob
|
|
// "blob 0\0" -> SHA1 = e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
assert_eq!(hash, "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
|
|
}
|
|
|
|
#[test]
|
|
fn blob_sha1_hello_world() {
|
|
// Verify against `echo -n "hello world" | git hash-object --stdin`
|
|
// "blob 11\0hello world" -> SHA1 = 95d09f2b10159347eece71399a7e2e907ea3df4f
|
|
let hash = compute_blob_sha1("hello world");
|
|
assert_eq!(hash, "95d09f2b10159347eece71399a7e2e907ea3df4f");
|
|
}
|
|
|
|
#[test]
|
|
fn blob_sha1_multiline_script() {
|
|
let content = "local foo = 1\nlocal bar = 2\nreturn foo + bar\n";
|
|
let hash = compute_blob_sha1(content);
|
|
assert_eq!(hash.len(), 40);
|
|
// Same content must produce same hash
|
|
assert_eq!(hash, compute_blob_sha1(content));
|
|
}
|
|
|
|
#[test]
|
|
fn blob_sha1_different_content_different_hash() {
|
|
let h1 = compute_blob_sha1("version 1");
|
|
let h2 = compute_blob_sha1("version 2");
|
|
assert_ne!(h1, h2);
|
|
}
|
|
|
|
#[test]
|
|
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 output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&dir.path().to_string_lossy(),
|
|
"hash-object",
|
|
"test.luau",
|
|
])
|
|
.output()
|
|
.unwrap();
|
|
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
|
|
let our_hash = compute_blob_sha1(content);
|
|
assert_eq!(our_hash, git_hash);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// git_repo_root
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn repo_root_not_a_repo() {
|
|
let dir = tempdir().unwrap();
|
|
assert!(git_repo_root(dir.path()).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn repo_root_valid_repo() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
let root = git_repo_root(dir.path()).unwrap();
|
|
assert!(root.exists());
|
|
}
|
|
|
|
#[test]
|
|
fn repo_root_from_subdirectory() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
let sub = dir.path().join("sub").join("deep");
|
|
fs::create_dir_all(&sub).unwrap();
|
|
let root = git_repo_root(&sub).unwrap();
|
|
// Subdirectory should resolve to the same repo root
|
|
assert_eq!(
|
|
root.canonicalize().unwrap(),
|
|
git_repo_root(dir.path()).unwrap().canonicalize().unwrap()
|
|
);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// git_changed_files
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn changed_files_no_changes() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("file.luau"), "content").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
|
|
let changed = git_changed_files(dir.path());
|
|
assert!(changed.is_some());
|
|
assert!(changed.unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_unstaged_modification() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "original").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("script.luau"), "modified").unwrap();
|
|
|
|
let changed = git_changed_files(dir.path()).unwrap();
|
|
assert!(changed.contains(&PathBuf::from("script.luau")));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_staged_modification() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "original").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("script.luau"), "staged version").unwrap();
|
|
git_stage(dir.path(), "script.luau");
|
|
|
|
let changed = git_changed_files(dir.path()).unwrap();
|
|
assert!(changed.contains(&PathBuf::from("script.luau")));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_untracked_file() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("existing.luau"), "content").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("new_file.luau"), "new").unwrap();
|
|
|
|
let changed = git_changed_files(dir.path()).unwrap();
|
|
assert!(changed.contains(&PathBuf::from("new_file.luau")));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_deleted_file() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("to_delete.luau"), "content").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::remove_file(dir.path().join("to_delete.luau")).unwrap();
|
|
|
|
let changed = git_changed_files(dir.path()).unwrap();
|
|
assert!(changed.contains(&PathBuf::from("to_delete.luau")));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_multiple_types() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("committed.luau"), "ok").unwrap();
|
|
fs::write(dir.path().join("will_modify.luau"), "old").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
|
|
fs::write(dir.path().join("will_modify.luau"), "new").unwrap();
|
|
fs::write(dir.path().join("untracked.luau"), "brand new").unwrap();
|
|
|
|
let changed = git_changed_files(dir.path()).unwrap();
|
|
assert!(changed.contains(&PathBuf::from("will_modify.luau")));
|
|
assert!(changed.contains(&PathBuf::from("untracked.luau")));
|
|
assert!(!changed.contains(&PathBuf::from("committed.luau")));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_no_head_returns_none() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
// No commits at all, but a file exists
|
|
fs::write(dir.path().join("new.luau"), "content").unwrap();
|
|
|
|
let changed = git_changed_files(dir.path());
|
|
// Should still return Some with the untracked file
|
|
assert!(changed.is_some());
|
|
assert!(changed.unwrap().contains(&PathBuf::from("new.luau")));
|
|
}
|
|
|
|
#[test]
|
|
fn changed_files_subdirectory() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::create_dir_all(dir.path().join("src")).unwrap();
|
|
fs::write(dir.path().join("src/module.luau"), "original").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("src/module.luau"), "modified").unwrap();
|
|
|
|
let changed = git_changed_files(dir.path()).unwrap();
|
|
assert!(changed.contains(&PathBuf::from("src/module.luau")));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// git_show_head / git_show_staged
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn show_head_committed_file() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "committed content").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
|
|
let content = git_show_head(dir.path(), Path::new("script.luau"));
|
|
assert_eq!(content.as_deref(), Some("committed content"));
|
|
}
|
|
|
|
#[test]
|
|
fn show_head_new_file_returns_none() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("dummy.luau"), "x").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
|
|
let content = git_show_head(dir.path(), Path::new("nonexistent.luau"));
|
|
assert!(content.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn show_head_returns_committed_not_working_tree() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "v1").unwrap();
|
|
git_commit_all(dir.path(), "v1");
|
|
fs::write(dir.path().join("script.luau"), "v2 modified").unwrap();
|
|
|
|
let content = git_show_head(dir.path(), Path::new("script.luau"));
|
|
assert_eq!(content.as_deref(), Some("v1"));
|
|
}
|
|
|
|
#[test]
|
|
fn show_staged_returns_staged_content() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "v1").unwrap();
|
|
git_commit_all(dir.path(), "v1");
|
|
fs::write(dir.path().join("script.luau"), "v2 staged").unwrap();
|
|
git_stage(dir.path(), "script.luau");
|
|
|
|
let content = git_show_staged(dir.path(), Path::new("script.luau"));
|
|
assert_eq!(content.as_deref(), Some("v2 staged"));
|
|
}
|
|
|
|
#[test]
|
|
fn show_staged_returns_none_for_unstaged() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "v1").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
|
|
// File not staged, show :0: should return committed version (same as HEAD)
|
|
let staged = git_show_staged(dir.path(), Path::new("script.luau"));
|
|
let head = git_show_head(dir.path(), Path::new("script.luau"));
|
|
assert_eq!(staged, head);
|
|
}
|
|
|
|
#[test]
|
|
fn show_staged_differs_from_head_when_staged() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("script.luau"), "v1").unwrap();
|
|
git_commit_all(dir.path(), "v1");
|
|
fs::write(dir.path().join("script.luau"), "v2 staged").unwrap();
|
|
git_stage(dir.path(), "script.luau");
|
|
|
|
let head = git_show_head(dir.path(), Path::new("script.luau")).unwrap();
|
|
let staged = git_show_staged(dir.path(), Path::new("script.luau")).unwrap();
|
|
assert_ne!(head, staged);
|
|
assert_eq!(head, "v1");
|
|
assert_eq!(staged, "v2 staged");
|
|
}
|
|
|
|
#[test]
|
|
fn show_head_subdirectory_path() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::create_dir_all(dir.path().join("src")).unwrap();
|
|
fs::write(dir.path().join("src/init.luau"), "init content").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
|
|
let content = git_show_head(dir.path(), Path::new("src/init.luau"));
|
|
assert_eq!(content.as_deref(), Some("init content"));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// git_add
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn git_add_stages_file() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("file.luau"), "content").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("file.luau"), "modified").unwrap();
|
|
|
|
assert!(!git_is_staged(dir.path(), "file.luau"));
|
|
git_add(dir.path(), &[PathBuf::from("file.luau")]);
|
|
assert!(git_is_staged(dir.path(), "file.luau"));
|
|
}
|
|
|
|
#[test]
|
|
fn git_add_stages_multiple_files() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("a.luau"), "a").unwrap();
|
|
fs::write(dir.path().join("b.luau"), "b").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("a.luau"), "a modified").unwrap();
|
|
fs::write(dir.path().join("b.luau"), "b modified").unwrap();
|
|
|
|
git_add(
|
|
dir.path(),
|
|
&[PathBuf::from("a.luau"), PathBuf::from("b.luau")],
|
|
);
|
|
assert!(git_is_staged(dir.path(), "a.luau"));
|
|
assert!(git_is_staged(dir.path(), "b.luau"));
|
|
}
|
|
|
|
#[test]
|
|
fn git_add_empty_paths_is_noop() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
// Should not panic or error
|
|
git_add(dir.path(), &[]);
|
|
}
|
|
|
|
#[test]
|
|
fn git_add_stages_new_file() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("existing.luau"), "x").unwrap();
|
|
git_commit_all(dir.path(), "init");
|
|
fs::write(dir.path().join("new.luau"), "new content").unwrap();
|
|
|
|
git_add(dir.path(), &[PathBuf::from("new.luau")]);
|
|
assert!(git_is_staged(dir.path(), "new.luau"));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// is_script_class
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn script_class_detection() {
|
|
assert!(is_script_class("Script"));
|
|
assert!(is_script_class("LocalScript"));
|
|
assert!(is_script_class("ModuleScript"));
|
|
assert!(!is_script_class("Folder"));
|
|
assert!(!is_script_class("Part"));
|
|
assert!(!is_script_class("Model"));
|
|
assert!(!is_script_class("StringValue"));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Hash consistency: our SHA1 matches git hash-object for various content
|
|
// -----------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn hash_consistency_empty_file() {
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("empty.luau"), "").unwrap();
|
|
|
|
let output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&dir.path().to_string_lossy(),
|
|
"hash-object",
|
|
"empty.luau",
|
|
])
|
|
.output()
|
|
.unwrap();
|
|
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
assert_eq!(compute_blob_sha1(""), git_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn hash_consistency_unicode_content() {
|
|
let content = "-- Unicode: 日本語テスト\nlocal x = '🎮'\n";
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("unicode.luau"), content).unwrap();
|
|
|
|
let output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&dir.path().to_string_lossy(),
|
|
"hash-object",
|
|
"unicode.luau",
|
|
])
|
|
.output()
|
|
.unwrap();
|
|
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
assert_eq!(compute_blob_sha1(content), git_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn hash_consistency_large_file() {
|
|
let content: String = (0..10000)
|
|
.map(|i| format!("local var_{} = {}\n", i, i))
|
|
.collect();
|
|
let dir = tempdir().unwrap();
|
|
git_init(dir.path());
|
|
fs::write(dir.path().join("large.luau"), &content).unwrap();
|
|
|
|
let output = Command::new("git")
|
|
.args([
|
|
"-C",
|
|
&dir.path().to_string_lossy(),
|
|
"hash-object",
|
|
"large.luau",
|
|
])
|
|
.output()
|
|
.unwrap();
|
|
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
assert_eq!(compute_blob_sha1(&content), git_hash);
|
|
}
|
|
}
|