mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 14:15:24 +00:00
Implement Syncback to support converting Roblox files to a Rojo project (#937)
This is a very large commit. Consider checking the linked PR for more information.
This commit is contained in:
@@ -1,17 +1,27 @@
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use memofs::{DirEntry, Vfs};
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource},
|
||||
syncback::{hash_instance, FsSnapshot, SyncbackReturn, SyncbackSnapshot},
|
||||
};
|
||||
|
||||
use super::{meta_file::DirectoryMetadata, snapshot_from_vfs};
|
||||
|
||||
const EMPTY_DIR_KEEP_NAME: &str = ".gitkeep";
|
||||
|
||||
pub fn snapshot_dir(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let mut snapshot = match snapshot_dir_no_meta(context, vfs, path)? {
|
||||
let mut snapshot = match snapshot_dir_no_meta(context, vfs, path, name)? {
|
||||
Some(snapshot) => snapshot,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -29,6 +39,7 @@ pub fn snapshot_dir_no_meta(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let passes_filter_rules = |child: &DirEntry| {
|
||||
context
|
||||
@@ -51,13 +62,6 @@ pub fn snapshot_dir_no_meta(
|
||||
}
|
||||
}
|
||||
|
||||
let instance_name = path
|
||||
.file_name()
|
||||
.expect("Could not extract file name")
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", path.display()))?
|
||||
.to_string();
|
||||
|
||||
let relevant_paths = vec![
|
||||
path.to_path_buf(),
|
||||
// TODO: We shouldn't need to know about Lua existing in this
|
||||
@@ -73,7 +77,7 @@ pub fn snapshot_dir_no_meta(
|
||||
];
|
||||
|
||||
let snapshot = InstanceSnapshot::new()
|
||||
.name(instance_name)
|
||||
.name(name)
|
||||
.class_name("Folder")
|
||||
.children(snapshot_children)
|
||||
.metadata(
|
||||
@@ -86,6 +90,136 @@ pub fn snapshot_dir_no_meta(
|
||||
Ok(Some(snapshot))
|
||||
}
|
||||
|
||||
pub fn syncback_dir<'sync>(
|
||||
snapshot: &SyncbackSnapshot<'sync>,
|
||||
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
||||
let new_inst = snapshot.new_inst();
|
||||
|
||||
let mut dir_syncback = syncback_dir_no_meta(snapshot)?;
|
||||
|
||||
let mut meta = DirectoryMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?;
|
||||
if let Some(meta) = &mut meta {
|
||||
if new_inst.class != "Folder" {
|
||||
meta.class_name = Some(new_inst.class);
|
||||
}
|
||||
|
||||
if !meta.is_empty() {
|
||||
dir_syncback.fs_snapshot.add_file(
|
||||
snapshot.path.join("init.meta.json"),
|
||||
serde_json::to_vec_pretty(&meta)
|
||||
.context("could not serialize new init.meta.json")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let metadata_empty = meta
|
||||
.as_ref()
|
||||
.map(DirectoryMetadata::is_empty)
|
||||
.unwrap_or_default();
|
||||
if new_inst.children().is_empty() && metadata_empty {
|
||||
dir_syncback
|
||||
.fs_snapshot
|
||||
.add_file(snapshot.path.join(EMPTY_DIR_KEEP_NAME), Vec::new())
|
||||
}
|
||||
|
||||
Ok(dir_syncback)
|
||||
}
|
||||
|
||||
pub fn syncback_dir_no_meta<'sync>(
|
||||
snapshot: &SyncbackSnapshot<'sync>,
|
||||
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
||||
let new_inst = snapshot.new_inst();
|
||||
|
||||
let mut children = Vec::new();
|
||||
let mut removed_children = Vec::new();
|
||||
|
||||
// We have to enforce unique child names for the file system.
|
||||
let mut child_names = HashSet::with_capacity(new_inst.children().len());
|
||||
let mut duplicate_set = HashSet::new();
|
||||
for child_ref in new_inst.children() {
|
||||
let child = snapshot.get_new_instance(*child_ref).unwrap();
|
||||
if !child_names.insert(child.name.to_lowercase()) {
|
||||
duplicate_set.insert(child.name.as_str());
|
||||
}
|
||||
}
|
||||
if !duplicate_set.is_empty() {
|
||||
if duplicate_set.len() <= 25 {
|
||||
anyhow::bail!(
|
||||
"Instance has children with duplicate name (case may not exactly match):\n {}",
|
||||
duplicate_set.into_iter().collect::<Vec<&str>>().join(", ")
|
||||
);
|
||||
}
|
||||
anyhow::bail!("Instance has more than 25 children with duplicate names");
|
||||
}
|
||||
|
||||
if let Some(old_inst) = snapshot.old_inst() {
|
||||
let mut old_child_map = HashMap::with_capacity(old_inst.children().len());
|
||||
for child in old_inst.children() {
|
||||
let inst = snapshot.get_old_instance(*child).unwrap();
|
||||
old_child_map.insert(inst.name(), inst);
|
||||
}
|
||||
|
||||
for new_child_ref in new_inst.children() {
|
||||
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
||||
if let Some(old_child) = old_child_map.remove(new_child.name.as_str()) {
|
||||
if old_child.metadata().relevant_paths.is_empty() {
|
||||
log::debug!(
|
||||
"Skipping instance {} because it doesn't exist on the disk",
|
||||
old_child.name()
|
||||
);
|
||||
continue;
|
||||
} else if matches!(
|
||||
old_child.metadata().instigating_source,
|
||||
Some(InstigatingSource::ProjectNode { .. })
|
||||
) {
|
||||
log::debug!(
|
||||
"Skipping instance {} because it originates in a project file",
|
||||
old_child.name()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// This child exists in both doms. Pass it on.
|
||||
children.push(snapshot.with_joined_path(*new_child_ref, Some(old_child.id()))?);
|
||||
} else {
|
||||
// The child only exists in the the new dom
|
||||
children.push(snapshot.with_joined_path(*new_child_ref, None)?);
|
||||
}
|
||||
}
|
||||
// Any children that are in the old dom but not the new one are removed.
|
||||
removed_children.extend(old_child_map.into_values());
|
||||
} else {
|
||||
// There is no old instance. Just add every child.
|
||||
for new_child_ref in new_inst.children() {
|
||||
children.push(snapshot.with_joined_path(*new_child_ref, None)?);
|
||||
}
|
||||
}
|
||||
let mut fs_snapshot = FsSnapshot::new();
|
||||
|
||||
if let Some(old_ref) = snapshot.old {
|
||||
let new_hash = hash_instance(snapshot.project(), snapshot.new_tree(), snapshot.new)
|
||||
.expect("new Instance should be hashable");
|
||||
let old_hash = hash_instance(snapshot.project(), snapshot.old_tree(), old_ref)
|
||||
.expect("old Instance should be hashable");
|
||||
|
||||
if old_hash != new_hash {
|
||||
fs_snapshot.add_dir(&snapshot.path);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Skipping reserializing directory {} because old and new tree hash the same",
|
||||
new_inst.name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fs_snapshot.add_dir(&snapshot.path);
|
||||
}
|
||||
|
||||
Ok(SyncbackReturn {
|
||||
fs_snapshot,
|
||||
children,
|
||||
removed_children,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
@@ -100,9 +234,10 @@ mod test {
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let instance_snapshot = snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot =
|
||||
snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo"), "foo")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
@@ -118,9 +253,10 @@ mod test {
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let instance_snapshot = snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot =
|
||||
snapshot_dir(&InstanceContext::default(), &vfs, Path::new("/foo"), "foo")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user