forked from rojo-rbx/rojo
Instead of bailing when children have duplicate filesystem names, syncback now resolves collisions by appending incrementing suffixes (e.g. Foo, Foo1, Foo2). This handles both init-renamed children and any other name collisions. Meta stem derivation is now path-based to correctly handle collision suffixes and dotted names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
135 lines
3.7 KiB
Rust
135 lines
3.7 KiB
Rust
use std::{path::Path, str};
|
|
|
|
use anyhow::Context as _;
|
|
use memofs::Vfs;
|
|
use rbx_dom_weak::types::Variant;
|
|
use rbx_dom_weak::ustr;
|
|
|
|
use crate::{
|
|
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
|
syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot},
|
|
};
|
|
|
|
use super::{meta_file::AdjacentMetadata, PathExt as _};
|
|
|
|
pub fn snapshot_txt(
|
|
context: &InstanceContext,
|
|
vfs: &Vfs,
|
|
path: &Path,
|
|
name: &str,
|
|
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
|
let contents = vfs.read_to_string(path)?;
|
|
let contents_str = contents.as_str();
|
|
|
|
let mut snapshot = InstanceSnapshot::new()
|
|
.name(name)
|
|
.class_name("StringValue")
|
|
.property(ustr("Value"), contents_str)
|
|
.metadata(
|
|
InstanceMetadata::new()
|
|
.instigating_source(path)
|
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
|
.context(context),
|
|
);
|
|
|
|
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
|
|
|
Ok(Some(snapshot))
|
|
}
|
|
|
|
pub fn syncback_txt<'sync>(
|
|
snapshot: &SyncbackSnapshot<'sync>,
|
|
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
|
let new_inst = snapshot.new_inst();
|
|
|
|
let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Value")) {
|
|
source.as_bytes().to_vec()
|
|
} else {
|
|
anyhow::bail!("StringValues must have a `Value` property that is a String");
|
|
};
|
|
let mut fs_snapshot = FsSnapshot::new();
|
|
fs_snapshot.add_file(&snapshot.path, contents);
|
|
|
|
let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?;
|
|
if let Some(mut meta) = meta {
|
|
// StringValues have relatively few properties that we care about, so
|
|
// shifting is fine.
|
|
meta.properties.shift_remove(&ustr("Value"));
|
|
|
|
if !meta.is_empty() {
|
|
let parent = snapshot.path.parent_err()?;
|
|
let file_name = snapshot
|
|
.path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("");
|
|
let meta_stem = file_name.strip_suffix(".txt").unwrap_or(file_name);
|
|
fs_snapshot.add_file(
|
|
parent.join(format!("{meta_stem}.meta.json")),
|
|
serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?,
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(SyncbackReturn {
|
|
fs_snapshot,
|
|
children: Vec::new(),
|
|
removed_children: Vec::new(),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
use memofs::{InMemoryFs, VfsSnapshot};
|
|
|
|
#[test]
|
|
fn instance_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs.clone());
|
|
|
|
let instance_snapshot = snapshot_txt(
|
|
&InstanceContext::default(),
|
|
&vfs,
|
|
Path::new("/foo.txt"),
|
|
"foo",
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
}
|
|
|
|
#[test]
|
|
fn with_metadata() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.txt", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/foo.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"{
|
|
"id": "manually specified"
|
|
}"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
let vfs = Vfs::new(imfs.clone());
|
|
|
|
let instance_snapshot = snapshot_txt(
|
|
&InstanceContext::default(),
|
|
&vfs,
|
|
Path::new("/foo.txt"),
|
|
"foo",
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
}
|
|
}
|