forked from rojo-rbx/rojo
Compare commits
1 Commits
feature/in
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
c552fdc52e
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,6 +23,3 @@
|
|||||||
# Macos file system junk
|
# Macos file system junk
|
||||||
._*
|
._*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|
||||||
# JetBrains IDEs
|
|
||||||
/.idea/
|
|
||||||
|
|||||||
@@ -31,14 +31,10 @@ Making a new release? Simply add the new header with the version and date undern
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
||||||
* Implemented support for the "name" property in meta/model JSON files. ([#1187])
|
|
||||||
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
||||||
* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201])
|
|
||||||
|
|
||||||
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
|
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
|
||||||
[#1187]: https://github.com/rojo-rbx/rojo/pull/1187
|
|
||||||
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
|
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
|
||||||
[#1201]: https://github.com/rojo-rbx/rojo/pull/1201
|
|
||||||
|
|
||||||
## [7.7.0-rc.1] (November 27th, 2025)
|
## [7.7.0-rc.1] (November 27th, 2025)
|
||||||
|
|
||||||
|
|||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1319,7 +1319,6 @@ dependencies = [
|
|||||||
"fs-err",
|
"fs-err",
|
||||||
"notify",
|
"notify",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# memofs Changelog
|
# memofs Changelog
|
||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
* Added `Vfs::canonicalize`. [#1201]
|
|
||||||
|
|
||||||
## 0.3.1 (2025-11-27)
|
## 0.3.1 (2025-11-27)
|
||||||
* Added `Vfs::exists`. [#1169]
|
* Added `Vfs::exists`. [#1169]
|
||||||
|
|||||||
@@ -19,6 +19,3 @@ crossbeam-channel = "0.5.12"
|
|||||||
fs-err = "2.11.0"
|
fs-err = "2.11.0"
|
||||||
notify = "4.0.17"
|
notify = "4.0.17"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3.10.1"
|
|
||||||
|
|||||||
@@ -232,33 +232,6 @@ impl VfsBackend for InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
|
|
||||||
// in MemoFS. The current implementation will error if no prepended absolute path
|
|
||||||
// is found. It really only normalizes paths within the provided path's context.
|
|
||||||
// Example: "/Users/username/project/../other/file.txt" ->
|
|
||||||
// "/Users/username/other/file.txt"
|
|
||||||
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
|
|
||||||
// This is not very robust. We should implement proper path normalization here or otherwise
|
|
||||||
// warn if we are missing context and can not fully canonicalize the path correctly.
|
|
||||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
|
||||||
let mut normalized = PathBuf::new();
|
|
||||||
for component in path.components() {
|
|
||||||
match component {
|
|
||||||
std::path::Component::ParentDir => {
|
|
||||||
normalized.pop();
|
|
||||||
}
|
|
||||||
std::path::Component::CurDir => {}
|
|
||||||
_ => normalized.push(component),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inner = self.inner.lock().unwrap();
|
|
||||||
match inner.entries.get(&normalized) {
|
|
||||||
Some(_) => Ok(normalized),
|
|
||||||
None => not_found(&normalized),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
|||||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||||
@@ -226,11 +225,6 @@ impl VfsInner {
|
|||||||
self.backend.metadata(path)
|
self.backend.metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.backend.canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
self.backend.event_receiver()
|
self.backend.event_receiver()
|
||||||
}
|
}
|
||||||
@@ -419,19 +413,6 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().metadata(path)
|
self.inner.lock().unwrap().metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalize a path via the underlying backend.
|
|
||||||
///
|
|
||||||
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
|
|
||||||
/// resolved against the backend's current working directory (if applicable) and errors are
|
|
||||||
/// surfaced directly from the backend.
|
|
||||||
///
|
|
||||||
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
|
|
||||||
#[inline]
|
|
||||||
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.lock().unwrap().canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
@@ -559,13 +540,6 @@ impl VfsLock<'_> {
|
|||||||
self.inner.metadata(path)
|
self.inner.metadata(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalize a path via the underlying backend.
|
|
||||||
#[inline]
|
|
||||||
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
self.inner.canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
@@ -581,9 +555,7 @@ impl VfsLock<'_> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot};
|
use crate::{InMemoryFs, Vfs, VfsSnapshot};
|
||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// https://github.com/rojo-rbx/rojo/issues/899
|
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||||
#[test]
|
#[test]
|
||||||
@@ -599,62 +571,4 @@ mod test {
|
|||||||
"bar\nfoo\n\n"
|
"bar\nfoo\n\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// https://github.com/rojo-rbx/rojo/issues/1200
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_in_memory_success() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
|
||||||
|
|
||||||
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
|
|
||||||
PathBuf::from("/test/file.txt")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
contents.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_in_memory_missing_errors() {
|
|
||||||
let imfs = InMemoryFs::new();
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let err = vfs.canonicalize("test").unwrap_err();
|
|
||||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_std_backend_success() {
|
|
||||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let file_path = dir.path().join("file.txt");
|
|
||||||
fs_err::write(&file_path, contents.to_string()).unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(StdBackend::new());
|
|
||||||
let canonicalized = vfs.canonicalize(&file_path).unwrap();
|
|
||||||
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
|
|
||||||
assert_eq!(
|
|
||||||
vfs.read_to_string(&canonicalized).unwrap().to_string(),
|
|
||||||
contents.to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonicalize_std_backend_missing_errors() {
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let file_path = dir.path().join("test");
|
|
||||||
|
|
||||||
let vfs = Vfs::new(StdBackend::new());
|
|
||||||
let err = vfs.canonicalize(&file_path).unwrap_err();
|
|
||||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||||
|
|
||||||
@@ -50,10 +50,6 @@ impl VfsBackend for NoopBackend {
|
|||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
|
|
||||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
crossbeam_channel::never()
|
crossbeam_channel::never()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,10 +106,6 @@ impl VfsBackend for StdBackend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
|
||||||
fs_err::canonicalize(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||||
self.watcher_receiver.clone()
|
self.watcher_receiver.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
source: tests/tests/build.rs
|
||||||
|
expression: contents
|
||||||
|
---
|
||||||
|
<roblox version="4">
|
||||||
|
<Item class="Folder" referent="0">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">json_model_legacy_name</string>
|
||||||
|
</Properties>
|
||||||
|
<Item class="Folder" referent="1">
|
||||||
|
<Properties>
|
||||||
|
<string name="Name">Expected Name</string>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/build.rs
|
|
||||||
assertion_line: 109
|
|
||||||
expression: contents
|
|
||||||
---
|
|
||||||
<roblox version="4">
|
|
||||||
<Item class="DataModel" referent="0">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">model_json_name_input</string>
|
|
||||||
</Properties>
|
|
||||||
<Item class="Workspace" referent="1">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">Workspace</string>
|
|
||||||
<bool name="NeedsPivotMigration">false</bool>
|
|
||||||
</Properties>
|
|
||||||
<Item class="StringValue" referent="2">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">/Bar</string>
|
|
||||||
</Properties>
|
|
||||||
</Item>
|
|
||||||
</Item>
|
|
||||||
</Item>
|
|
||||||
</roblox>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/build.rs
|
|
||||||
assertion_line: 108
|
|
||||||
expression: contents
|
|
||||||
---
|
|
||||||
<roblox version="4">
|
|
||||||
<Item class="Folder" referent="0">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">slugified_name_roundtrip</string>
|
|
||||||
</Properties>
|
|
||||||
<Item class="Script" referent="1">
|
|
||||||
<Properties>
|
|
||||||
<string name="Name">/Script</string>
|
|
||||||
<token name="RunContext">0</token>
|
|
||||||
<string name="Source"><![CDATA[print("Hello world!")
|
|
||||||
]]></string>
|
|
||||||
</Properties>
|
|
||||||
</Item>
|
|
||||||
</Item>
|
|
||||||
</roblox>
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "json_model_legacy_name",
|
||||||
|
"tree": {
|
||||||
|
"$path": "folder"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Name": "Overridden Name",
|
||||||
|
"ClassName": "Folder"
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "model_json_name_input",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Bar",
|
|
||||||
"className": "StringValue"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
print("Hello world!")
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "slugified_name_roundtrip",
|
|
||||||
"tree": {
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
print("Hello world!")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/rojo_test/syncback_util.rs
|
|
||||||
assertion_line: 101
|
|
||||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
source: tests/rojo_test/syncback_util.rs
|
||||||
|
expression: src/ChildWithDuplicates.rbxm
|
||||||
|
---
|
||||||
|
num_types: 1
|
||||||
|
num_instances: 3
|
||||||
|
chunks:
|
||||||
|
- Inst:
|
||||||
|
type_id: 0
|
||||||
|
type_name: Folder
|
||||||
|
object_format: 0
|
||||||
|
referents:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- Prop:
|
||||||
|
type_id: 0
|
||||||
|
prop_name: AttributesSerialize
|
||||||
|
prop_type: String
|
||||||
|
values:
|
||||||
|
- ""
|
||||||
|
- ""
|
||||||
|
- ""
|
||||||
|
- Prop:
|
||||||
|
type_id: 0
|
||||||
|
prop_name: Capabilities
|
||||||
|
prop_type: SecurityCapabilities
|
||||||
|
values:
|
||||||
|
- 0
|
||||||
|
- 0
|
||||||
|
- 0
|
||||||
|
- Prop:
|
||||||
|
type_id: 0
|
||||||
|
prop_name: Name
|
||||||
|
prop_type: String
|
||||||
|
values:
|
||||||
|
- DuplicateChild
|
||||||
|
- DuplicateChild
|
||||||
|
- ChildWithDuplicates
|
||||||
|
- Prop:
|
||||||
|
type_id: 0
|
||||||
|
prop_name: DefinesCapabilities
|
||||||
|
prop_type: Bool
|
||||||
|
values:
|
||||||
|
- false
|
||||||
|
- false
|
||||||
|
- false
|
||||||
|
- Prop:
|
||||||
|
type_id: 0
|
||||||
|
prop_name: SourceAssetId
|
||||||
|
prop_type: Int64
|
||||||
|
values:
|
||||||
|
- -1
|
||||||
|
- -1
|
||||||
|
- -1
|
||||||
|
- Prop:
|
||||||
|
type_id: 0
|
||||||
|
prop_name: Tags
|
||||||
|
prop_type: String
|
||||||
|
values:
|
||||||
|
- ""
|
||||||
|
- ""
|
||||||
|
- ""
|
||||||
|
- Prnt:
|
||||||
|
version: 0
|
||||||
|
links:
|
||||||
|
- - 0
|
||||||
|
- 2
|
||||||
|
- - 1
|
||||||
|
- 2
|
||||||
|
- - 2
|
||||||
|
- -1
|
||||||
|
- End
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
---
|
---
|
||||||
source: tests/rojo_test/syncback_util.rs
|
source: tests/rojo_test/syncback_util.rs
|
||||||
assertion_line: 101
|
|
||||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||||
---
|
---
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild/.gitkeep
|
Writing src/ChildWithDuplicates.rbxm
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild1/.gitkeep
|
|
||||||
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild
|
|
||||||
Writing src/ChildWithDuplicates/DuplicateChild1
|
|
||||||
Writing src/ChildWithoutDuplicates
|
Writing src/ChildWithoutDuplicates
|
||||||
Writing src/ChildWithoutDuplicates/Child
|
Writing src/ChildWithoutDuplicates/Child
|
||||||
|
Removing src/ChildWithDuplicates
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/rojo_test/syncback_util.rs
|
|
||||||
assertion_line: 101
|
|
||||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
|
||||||
---
|
|
||||||
Writing default.project.json
|
|
||||||
Writing src/Camera.rbxm
|
|
||||||
Writing src/Terrain.rbxm
|
|
||||||
Writing src/_Folder/init.meta.json
|
|
||||||
Writing src/_Script.meta.json
|
|
||||||
Writing src/_Script.server.luau
|
|
||||||
Writing src
|
|
||||||
Writing src/_Folder
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/foo.model.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Bar",
|
|
||||||
"className": "StringValue"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/ChildWithDuplicates/DuplicateChild1/.gitkeep
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/ChildWithDuplicates/DuplicateChild/.gitkeep
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Folder.model.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"className": "Folder"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Folder/init.meta.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Folder"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script.meta.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script.server.luau
|
|
||||||
---
|
|
||||||
print("Hello world!")
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script/init.meta.json
|
|
||||||
---
|
|
||||||
{
|
|
||||||
"name": "/Script"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: tests/tests/syncback.rs
|
|
||||||
assertion_line: 31
|
|
||||||
expression: src/_Script/init.server.luau
|
|
||||||
---
|
|
||||||
print("Hello world!")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "model_json_name",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "/Bar",
|
|
||||||
"className": "StringValue"
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "slugified_name",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,13 +1,13 @@
|
|||||||
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
|
||||||
use jod_thread::JoinHandle;
|
|
||||||
use memofs::{IoResultExt, Vfs, VfsEvent};
|
|
||||||
use rbx_dom_weak::types::{Ref, Variant};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crossbeam_channel::{select, Receiver, RecvError, Sender};
|
||||||
|
use jod_thread::JoinHandle;
|
||||||
|
use memofs::{IoResultExt, Vfs, VfsEvent};
|
||||||
|
use rbx_dom_weak::types::{Ref, Variant};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
message_queue::MessageQueue,
|
message_queue::MessageQueue,
|
||||||
snapshot::{
|
snapshot::{
|
||||||
@@ -114,49 +114,6 @@ struct JobThreadContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl JobThreadContext {
|
impl JobThreadContext {
|
||||||
/// Computes and applies patches to the DOM for a given file path.
|
|
||||||
///
|
|
||||||
/// This function finds the nearest ancestor to the given path that has associated instances
|
|
||||||
/// in the tree.
|
|
||||||
/// It then computes and applies changes for each affected instance ID and
|
|
||||||
/// returns a vector of applied patch sets.
|
|
||||||
fn apply_patches(&self, path: PathBuf) -> Vec<AppliedPatchSet> {
|
|
||||||
let mut tree = self.tree.lock().unwrap();
|
|
||||||
let mut applied_patches = Vec::new();
|
|
||||||
|
|
||||||
// Find the nearest ancestor to this path that has
|
|
||||||
// associated instances in the tree. This helps make sure
|
|
||||||
// that we handle additions correctly, especially if we
|
|
||||||
// receive events for descendants of a large tree being
|
|
||||||
// created all at once.
|
|
||||||
let mut current_path = path.as_path();
|
|
||||||
let affected_ids = loop {
|
|
||||||
let ids = tree.get_ids_at_path(current_path);
|
|
||||||
|
|
||||||
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
|
||||||
|
|
||||||
if !ids.is_empty() {
|
|
||||||
break ids.to_vec();
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("Trying parent path...");
|
|
||||||
match current_path.parent() {
|
|
||||||
Some(parent) => current_path = parent,
|
|
||||||
None => break Vec::new(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for id in affected_ids {
|
|
||||||
if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) {
|
|
||||||
if !patch.is_empty() {
|
|
||||||
applied_patches.push(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applied_patches
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_vfs_event(&self, event: VfsEvent) {
|
fn handle_vfs_event(&self, event: VfsEvent) {
|
||||||
log::trace!("Vfs event: {:?}", event);
|
log::trace!("Vfs event: {:?}", event);
|
||||||
|
|
||||||
@@ -168,16 +125,41 @@ impl JobThreadContext {
|
|||||||
// For a given VFS event, we might have many changes to different parts
|
// For a given VFS event, we might have many changes to different parts
|
||||||
// of the tree. Calculate and apply all of these changes.
|
// of the tree. Calculate and apply all of these changes.
|
||||||
let applied_patches = match event {
|
let applied_patches = match event {
|
||||||
VfsEvent::Create(path) | VfsEvent::Write(path) => {
|
VfsEvent::Create(path) | VfsEvent::Remove(path) | VfsEvent::Write(path) => {
|
||||||
self.apply_patches(self.vfs.canonicalize(&path).unwrap())
|
let mut tree = self.tree.lock().unwrap();
|
||||||
}
|
let mut applied_patches = Vec::new();
|
||||||
VfsEvent::Remove(path) => {
|
|
||||||
// MemoFS does not track parent removals yet, so we can canonicalize
|
// Find the nearest ancestor to this path that has
|
||||||
// the parent path safely and then append the removed path's file name.
|
// associated instances in the tree. This helps make sure
|
||||||
let parent = path.parent().unwrap();
|
// that we handle additions correctly, especially if we
|
||||||
let file_name = path.file_name().unwrap();
|
// receive events for descendants of a large tree being
|
||||||
let parent_normalized = self.vfs.canonicalize(parent).unwrap();
|
// created all at once.
|
||||||
self.apply_patches(parent_normalized.join(file_name))
|
let mut current_path = path.as_path();
|
||||||
|
let affected_ids = loop {
|
||||||
|
let ids = tree.get_ids_at_path(current_path);
|
||||||
|
|
||||||
|
log::trace!("Path {} affects IDs {:?}", current_path.display(), ids);
|
||||||
|
|
||||||
|
if !ids.is_empty() {
|
||||||
|
break ids.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("Trying parent path...");
|
||||||
|
match current_path.parent() {
|
||||||
|
Some(parent) => current_path = parent,
|
||||||
|
None => break Vec::new(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for id in affected_ids {
|
||||||
|
if let Some(patch) = compute_and_apply_changes(&mut tree, &self.vfs, id) {
|
||||||
|
if !patch.is_empty() {
|
||||||
|
applied_patches.push(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applied_patches
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("Unhandled VFS event: {:?}", event);
|
log::warn!("Unhandled VFS event: {:?}", event);
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ pub struct SyncbackCommand {
|
|||||||
/// If provided, the prompt for writing to the file system is skipped.
|
/// If provided, the prompt for writing to the file system is skipped.
|
||||||
#[clap(long, short = 'y')]
|
#[clap(long, short = 'y')]
|
||||||
pub non_interactive: bool,
|
pub non_interactive: bool,
|
||||||
|
|
||||||
|
/// If provided, forces syncback to use JSON model files instead of binary
|
||||||
|
/// .rbxm files for instances that would otherwise serialize as binary.
|
||||||
|
#[clap(long)]
|
||||||
|
pub dangerously_force_json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncbackCommand {
|
impl SyncbackCommand {
|
||||||
@@ -104,6 +109,7 @@ impl SyncbackCommand {
|
|||||||
&mut dom_old,
|
&mut dom_old,
|
||||||
dom_new,
|
dom_new,
|
||||||
session_old.root_project(),
|
session_old.root_project(),
|
||||||
|
self.dangerously_force_json,
|
||||||
)?;
|
)?;
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Syncback finished in {:.02}s!",
|
"Syncback finished in {:.02}s!",
|
||||||
|
|||||||
@@ -70,12 +70,6 @@ pub struct InstanceMetadata {
|
|||||||
/// A schema provided via a JSON file, if one exists. Will be `None` for
|
/// A schema provided via a JSON file, if one exists. Will be `None` for
|
||||||
/// all non-JSON middleware.
|
/// all non-JSON middleware.
|
||||||
pub schema: Option<String>,
|
pub schema: Option<String>,
|
||||||
|
|
||||||
/// A custom name specified via meta.json or model.json files. If present,
|
|
||||||
/// this name will be used for the instance while the filesystem name will
|
|
||||||
/// be slugified to remove illegal characters.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub specified_name: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceMetadata {
|
impl InstanceMetadata {
|
||||||
@@ -88,7 +82,6 @@ impl InstanceMetadata {
|
|||||||
specified_id: None,
|
specified_id: None,
|
||||||
middleware: None,
|
middleware: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
specified_name: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +130,6 @@ impl InstanceMetadata {
|
|||||||
pub fn schema(self, schema: Option<String>) -> Self {
|
pub fn schema(self, schema: Option<String>) -> Self {
|
||||||
Self { schema, ..self }
|
Self { schema, ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn specified_name(self, specified_name: Option<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
specified_name,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InstanceMetadata {
|
impl Default for InstanceMetadata {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub fn snapshot_csv(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?]),
|
.relevant_paths(vec![path.to_path_buf()]),
|
||||||
);
|
);
|
||||||
|
|
||||||
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
||||||
@@ -109,14 +109,8 @@ pub fn syncback_csv<'sync>(
|
|||||||
|
|
||||||
if !meta.is_empty() {
|
if !meta.is_empty() {
|
||||||
let parent = snapshot.path.parent_err()?;
|
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(".csv").unwrap_or(file_name);
|
|
||||||
fs_snapshot.add_file(
|
fs_snapshot.add_file(
|
||||||
parent.join(format!("{meta_stem}.meta.json")),
|
parent.join(format!("{}.meta.json", new_inst.name)),
|
||||||
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,10 @@ use memofs::{DirEntry, Vfs};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource},
|
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource},
|
||||||
syncback::{
|
syncback::{hash_instance, FsSnapshot, SyncbackReturn, SyncbackSnapshot},
|
||||||
extension_for_middleware, hash_instance, FsSnapshot, SyncbackReturn,
|
|
||||||
SyncbackSnapshot,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{meta_file::DirectoryMetadata, snapshot_from_vfs, Middleware};
|
use super::{meta_file::DirectoryMetadata, snapshot_from_vfs};
|
||||||
|
|
||||||
const EMPTY_DIR_KEEP_NAME: &str = ".gitkeep";
|
const EMPTY_DIR_KEEP_NAME: &str = ".gitkeep";
|
||||||
|
|
||||||
@@ -65,19 +62,18 @@ pub fn snapshot_dir_no_meta(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalized_path = vfs.canonicalize(path)?;
|
|
||||||
let relevant_paths = vec![
|
let relevant_paths = vec![
|
||||||
normalized_path.clone(),
|
path.to_path_buf(),
|
||||||
// TODO: We shouldn't need to know about Lua existing in this
|
// TODO: We shouldn't need to know about Lua existing in this
|
||||||
// middleware. Should we figure out a way for that function to add
|
// middleware. Should we figure out a way for that function to add
|
||||||
// relevant paths to this middleware?
|
// relevant paths to this middleware?
|
||||||
normalized_path.join("init.lua"),
|
path.join("init.lua"),
|
||||||
normalized_path.join("init.luau"),
|
path.join("init.luau"),
|
||||||
normalized_path.join("init.server.lua"),
|
path.join("init.server.lua"),
|
||||||
normalized_path.join("init.server.luau"),
|
path.join("init.server.luau"),
|
||||||
normalized_path.join("init.client.lua"),
|
path.join("init.client.lua"),
|
||||||
normalized_path.join("init.client.luau"),
|
path.join("init.client.luau"),
|
||||||
normalized_path.join("init.csv"),
|
path.join("init.csv"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let snapshot = InstanceSnapshot::new()
|
let snapshot = InstanceSnapshot::new()
|
||||||
@@ -94,22 +90,6 @@ pub fn snapshot_dir_no_meta(
|
|||||||
Ok(Some(snapshot))
|
Ok(Some(snapshot))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a filesystem name into (stem, extension) based on middleware type.
|
|
||||||
/// For directory middleware, the extension is empty. For file middleware,
|
|
||||||
/// the extension comes from `extension_for_middleware`.
|
|
||||||
fn split_name_and_ext(name: &str, middleware: Middleware) -> (&str, &str) {
|
|
||||||
if middleware.is_dir() {
|
|
||||||
(name, "")
|
|
||||||
} else {
|
|
||||||
let ext = extension_for_middleware(middleware);
|
|
||||||
if let Some(stem) = name.strip_suffix(&format!(".{ext}")) {
|
|
||||||
(stem, ext)
|
|
||||||
} else {
|
|
||||||
(name, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn syncback_dir<'sync>(
|
pub fn syncback_dir<'sync>(
|
||||||
snapshot: &SyncbackSnapshot<'sync>,
|
snapshot: &SyncbackSnapshot<'sync>,
|
||||||
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
||||||
@@ -153,128 +133,65 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
let mut removed_children = Vec::new();
|
let mut removed_children = Vec::new();
|
||||||
|
|
||||||
// Build the old child map early so it can be used for deduplication below.
|
// We have to enforce unique child names for the file system.
|
||||||
let mut old_child_map = HashMap::new();
|
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() {
|
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() {
|
for child in old_inst.children() {
|
||||||
let inst = snapshot.get_old_instance(*child).unwrap();
|
let inst = snapshot.get_old_instance(*child).unwrap();
|
||||||
old_child_map.insert(inst.name(), inst);
|
old_child_map.insert(inst.name(), inst);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- Two-pass collision resolution ---
|
for new_child_ref in new_inst.children() {
|
||||||
//
|
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
||||||
// Pass 1: Collect each child's base filesystem name and old ref, applying
|
if let Some(old_child) = old_child_map.remove(new_child.name.as_str()) {
|
||||||
// skip conditions. Track which names are used (lowercased) so we can
|
if old_child.metadata().relevant_paths.is_empty() {
|
||||||
// detect collisions.
|
log::debug!(
|
||||||
struct ChildEntry {
|
"Skipping instance {} because it doesn't exist on the disk",
|
||||||
new_ref: rbx_dom_weak::types::Ref,
|
old_child.name()
|
||||||
old_ref: Option<rbx_dom_weak::types::Ref>,
|
);
|
||||||
base_name: String,
|
continue;
|
||||||
middleware: Middleware,
|
} else if matches!(
|
||||||
skip: bool,
|
old_child.metadata().instigating_source,
|
||||||
}
|
Some(InstigatingSource::ProjectNode { .. })
|
||||||
|
) {
|
||||||
let mut entries = Vec::with_capacity(new_inst.children().len());
|
log::debug!(
|
||||||
let mut used_names: HashSet<String> = HashSet::with_capacity(new_inst.children().len());
|
"Skipping instance {} because it originates in a project file",
|
||||||
let mut collision_indices: Vec<usize> = Vec::new();
|
old_child.name()
|
||||||
|
);
|
||||||
for new_child_ref in new_inst.children() {
|
continue;
|
||||||
let new_child = snapshot.get_new_instance(*new_child_ref).unwrap();
|
}
|
||||||
|
// This child exists in both doms. Pass it on.
|
||||||
// Determine old_ref and apply skip conditions.
|
children.push(snapshot.with_joined_path(*new_child_ref, Some(old_child.id()))?);
|
||||||
let old_child = if snapshot.old_inst().is_some() {
|
|
||||||
old_child_map.remove(new_child.name.as_str())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut skip = false;
|
|
||||||
if let Some(ref old) = old_child {
|
|
||||||
if old.metadata().relevant_paths.is_empty() {
|
|
||||||
log::debug!(
|
|
||||||
"Skipping instance {} because it doesn't exist on the disk",
|
|
||||||
old.name()
|
|
||||||
);
|
|
||||||
skip = true;
|
|
||||||
} else if matches!(
|
|
||||||
old.metadata().instigating_source,
|
|
||||||
Some(InstigatingSource::ProjectNode { .. })
|
|
||||||
) {
|
|
||||||
log::debug!(
|
|
||||||
"Skipping instance {} because it originates in a project file",
|
|
||||||
old.name()
|
|
||||||
);
|
|
||||||
skip = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let old_ref = old_child.as_ref().map(|o| o.id());
|
|
||||||
|
|
||||||
if skip {
|
|
||||||
entries.push(ChildEntry {
|
|
||||||
new_ref: *new_child_ref,
|
|
||||||
old_ref,
|
|
||||||
base_name: String::new(),
|
|
||||||
middleware: Middleware::Dir,
|
|
||||||
skip: true,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (middleware, base_name) =
|
|
||||||
snapshot.child_middleware_and_name(*new_child_ref, old_ref)?;
|
|
||||||
|
|
||||||
let idx = entries.len();
|
|
||||||
let lower = base_name.to_lowercase();
|
|
||||||
if !used_names.insert(lower) {
|
|
||||||
// Name already claimed — needs resolution.
|
|
||||||
collision_indices.push(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(ChildEntry {
|
|
||||||
new_ref: *new_child_ref,
|
|
||||||
old_ref,
|
|
||||||
base_name,
|
|
||||||
middleware,
|
|
||||||
skip: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: Resolve collisions by appending incrementing suffixes.
|
|
||||||
for idx in collision_indices {
|
|
||||||
let entry = &entries[idx];
|
|
||||||
let (stem, ext) = split_name_and_ext(&entry.base_name, entry.middleware);
|
|
||||||
let mut counter = 1u32;
|
|
||||||
loop {
|
|
||||||
let candidate = if ext.is_empty() {
|
|
||||||
format!("{stem}{counter}")
|
|
||||||
} else {
|
} else {
|
||||||
format!("{stem}{counter}.{ext}")
|
// The child only exists in the the new dom
|
||||||
};
|
children.push(snapshot.with_joined_path(*new_child_ref, None)?);
|
||||||
let lower = candidate.to_lowercase();
|
|
||||||
if used_names.insert(lower) {
|
|
||||||
// Safe to mutate — we only visit each collision index once.
|
|
||||||
let entry = &mut entries[idx];
|
|
||||||
entry.base_name = candidate;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
counter += 1;
|
|
||||||
}
|
}
|
||||||
}
|
// Any children that are in the old dom but not the new one are removed.
|
||||||
|
|
||||||
// Create snapshots from resolved entries.
|
|
||||||
for entry in &entries {
|
|
||||||
if entry.skip {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let resolved_path = snapshot.path.join(&entry.base_name);
|
|
||||||
children.push(snapshot.with_new_path(resolved_path, entry.new_ref, entry.old_ref));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any children that are in the old dom but not the new one are removed.
|
|
||||||
if snapshot.old_inst().is_some() {
|
|
||||||
removed_children.extend(old_child_map.into_values());
|
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();
|
let mut fs_snapshot = FsSnapshot::new();
|
||||||
|
|
||||||
@@ -307,12 +224,6 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
snapshot::{InstanceMetadata, InstanceSnapshot},
|
|
||||||
Project, RojoTree, SyncbackData, SyncbackSnapshot,
|
|
||||||
};
|
|
||||||
use memofs::{InMemoryFs, VfsSnapshot};
|
use memofs::{InMemoryFs, VfsSnapshot};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -349,302 +260,4 @@ mod test {
|
|||||||
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_project() -> Project {
|
|
||||||
serde_json::from_str(r#"{"tree": {"$className": "DataModel"}}"#).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_vfs() -> Vfs {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/root", VfsSnapshot::empty_dir()).unwrap();
|
|
||||||
Vfs::new(imfs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two children whose Roblox names are identical when lowercased ("Alpha"
|
|
||||||
/// and "alpha") but live at different filesystem paths because of the
|
|
||||||
/// `name` property ("Beta/" and "Alpha/" respectively). The dedup check
|
|
||||||
/// must use the actual filesystem paths, not the raw Roblox names, to
|
|
||||||
/// avoid a false-positive duplicate error.
|
|
||||||
#[test]
|
|
||||||
fn syncback_no_false_duplicate_with_name_prop() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
// Old child A: Roblox name "Alpha", on disk at "/root/Beta"
|
|
||||||
// (name property maps "Alpha" → "Beta" on the filesystem)
|
|
||||||
let old_child_a = InstanceSnapshot::new()
|
|
||||||
.name("Alpha")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root/Beta"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root/Beta")]),
|
|
||||||
);
|
|
||||||
// Old child B: Roblox name "alpha", on disk at "/root/Alpha"
|
|
||||||
let old_child_b = InstanceSnapshot::new()
|
|
||||||
.name("alpha")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root/Alpha"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root/Alpha")]),
|
|
||||||
);
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.children(vec![old_child_a, old_child_b])
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
// New state: same two children in Roblox.
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Alpha"));
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("alpha"));
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should not error when two children have the same lowercased Roblox \
|
|
||||||
name but map to distinct filesystem paths: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two completely new children with the same name get resolved via
|
|
||||||
/// incrementing suffixes instead of erroring.
|
|
||||||
#[test]
|
|
||||||
fn syncback_resolves_sibling_duplicate_names() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Foo"));
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Foo"));
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should resolve duplicate names with suffixes, not error: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
let children = result.unwrap().children;
|
|
||||||
let mut names: Vec<String> = children
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.path.file_name().unwrap().to_string_lossy().into_owned())
|
|
||||||
.collect();
|
|
||||||
names.sort();
|
|
||||||
assert_eq!(names, vec!["Foo", "Foo1"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A new child named "Init" (as a ModuleScript) would naively become
|
|
||||||
/// "Init.luau", which case-insensitively matches the parent's reserved
|
|
||||||
/// "init.luau". Syncback must resolve this automatically by prefixing the
|
|
||||||
/// filesystem name with '_' (→ "_Init.luau") rather than erroring.
|
|
||||||
#[test]
|
|
||||||
fn syncback_resolves_init_name_conflict() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(
|
|
||||||
new_parent,
|
|
||||||
InstanceBuilder::new("ModuleScript").with_name("Init"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should resolve init-name conflict by prefixing '_', not error: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
// The child should have been placed at "_Init.luau", not "Init.luau".
|
|
||||||
let child_file_name = result
|
|
||||||
.unwrap()
|
|
||||||
.children
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.and_then(|c| c.path.file_name().map(|n| n.to_string_lossy().into_owned()))
|
|
||||||
.unwrap_or_default();
|
|
||||||
assert!(
|
|
||||||
child_file_name.starts_with('_'),
|
|
||||||
"child filesystem name should start with '_' to avoid init collision, \
|
|
||||||
got: {child_file_name}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A child whose filesystem name is stored with a slugified prefix (e.g.
|
|
||||||
/// "_Init") must NOT be blocked — only the bare "init" stem is reserved.
|
|
||||||
#[test]
|
|
||||||
fn syncback_allows_slugified_init_name() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
// Existing child: on disk as "_Init" (slugified from a name with an
|
|
||||||
// illegal character), its stem is "_init" which is not reserved.
|
|
||||||
let old_child = InstanceSnapshot::new()
|
|
||||||
.name("Init")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root/_Init"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root/_Init")]),
|
|
||||||
);
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.children(vec![old_child])
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(new_parent, InstanceBuilder::new("Folder").with_name("Init"));
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should allow a child whose filesystem name is slugified away from \
|
|
||||||
the reserved 'init' stem: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two new children both named "Init" (ModuleScripts) should get
|
|
||||||
/// "_Init.luau" and "_Init1.luau" respectively.
|
|
||||||
#[test]
|
|
||||||
fn syncback_resolves_multiple_init_conflicts() {
|
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
|
||||||
|
|
||||||
let old_parent = InstanceSnapshot::new()
|
|
||||||
.name("Parent")
|
|
||||||
.class_name("Folder")
|
|
||||||
.metadata(
|
|
||||||
InstanceMetadata::new()
|
|
||||||
.instigating_source(PathBuf::from("/root"))
|
|
||||||
.relevant_paths(vec![PathBuf::from("/root")]),
|
|
||||||
);
|
|
||||||
let old_tree = RojoTree::new(old_parent);
|
|
||||||
|
|
||||||
let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT"));
|
|
||||||
let new_parent = new_tree.insert(
|
|
||||||
new_tree.root_ref(),
|
|
||||||
InstanceBuilder::new("Folder").with_name("Parent"),
|
|
||||||
);
|
|
||||||
new_tree.insert(
|
|
||||||
new_parent,
|
|
||||||
InstanceBuilder::new("ModuleScript").with_name("Init"),
|
|
||||||
);
|
|
||||||
new_tree.insert(
|
|
||||||
new_parent,
|
|
||||||
InstanceBuilder::new("ModuleScript").with_name("Init"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let vfs = make_vfs();
|
|
||||||
let project = make_project();
|
|
||||||
let data = SyncbackData::for_test(&vfs, &old_tree, &new_tree, &project);
|
|
||||||
let snapshot = SyncbackSnapshot {
|
|
||||||
data,
|
|
||||||
old: Some(old_tree.get_root_id()),
|
|
||||||
new: new_parent,
|
|
||||||
path: PathBuf::from("/root"),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = syncback_dir_no_meta(&snapshot);
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"should resolve multiple init conflicts with suffixes: {:?}",
|
|
||||||
result.as_ref().err(),
|
|
||||||
);
|
|
||||||
let children = result.unwrap().children;
|
|
||||||
let mut names: Vec<String> = children
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.path.file_name().unwrap().to_string_lossy().into_owned())
|
|
||||||
.collect();
|
|
||||||
names.sort();
|
|
||||||
assert_eq!(names, vec!["_Init.luau", "_Init1.luau"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub fn snapshot_json(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,14 +35,20 @@ pub fn snapshot_json_model(
|
|||||||
format!("File is not a valid JSON model: {}", path.display())
|
format!("File is not a valid JSON model: {}", path.display())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// If the JSON has a name property, preserve it in metadata for syncback
|
if let Some(top_level_name) = &instance.name {
|
||||||
let specified_name = instance.name.clone();
|
let new_name = format!("{}.model.json", top_level_name);
|
||||||
|
|
||||||
// Use the name from JSON if present, otherwise fall back to filename-derived name
|
log::warn!(
|
||||||
if instance.name.is_none() {
|
"Model at path {} had a top-level Name field. \
|
||||||
instance.name = Some(name.to_owned());
|
This field has been ignored since Rojo 6.0.\n\
|
||||||
|
Consider removing this field and renaming the file to {}.",
|
||||||
|
new_name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance.name = Some(name.to_owned());
|
||||||
|
|
||||||
let id = instance.id.take().map(RojoRef::new);
|
let id = instance.id.take().map(RojoRef::new);
|
||||||
let schema = instance.schema.take();
|
let schema = instance.schema.take();
|
||||||
|
|
||||||
@@ -53,11 +59,10 @@ pub fn snapshot_json_model(
|
|||||||
snapshot.metadata = snapshot
|
snapshot.metadata = snapshot
|
||||||
.metadata
|
.metadata
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context)
|
.context(context)
|
||||||
.specified_id(id)
|
.specified_id(id)
|
||||||
.schema(schema)
|
.schema(schema);
|
||||||
.specified_name(specified_name);
|
|
||||||
|
|
||||||
Ok(Some(snapshot))
|
Ok(Some(snapshot))
|
||||||
}
|
}
|
||||||
@@ -76,7 +81,6 @@ pub fn syncback_json_model<'sync>(
|
|||||||
// schemas will ever exist in one project for it to matter, but it
|
// schemas will ever exist in one project for it to matter, but it
|
||||||
// could have a performance cost.
|
// could have a performance cost.
|
||||||
model.schema = old_inst.metadata().schema.clone();
|
model.schema = old_inst.metadata().schema.clone();
|
||||||
model.name = old_inst.metadata().specified_name.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SyncbackReturn {
|
Ok(SyncbackReturn {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub fn snapshot_lua(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -158,23 +158,8 @@ pub fn syncback_lua<'sync>(
|
|||||||
|
|
||||||
if !meta.is_empty() {
|
if !meta.is_empty() {
|
||||||
let parent_location = snapshot.path.parent_err()?;
|
let parent_location = 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(".server.luau")
|
|
||||||
.or_else(|| file_name.strip_suffix(".server.lua"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".client.luau"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".client.lua"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".plugin.luau"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".plugin.lua"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".luau"))
|
|
||||||
.or_else(|| file_name.strip_suffix(".lua"))
|
|
||||||
.unwrap_or(file_name);
|
|
||||||
fs_snapshot.add_file(
|
fs_snapshot.add_file(
|
||||||
parent_location.join(format!("{meta_stem}.meta.json")),
|
parent_location.join(format!("{}.meta.json", new_inst.name)),
|
||||||
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ use rbx_dom_weak::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json,
|
json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, syncback::SyncbackSnapshot,
|
||||||
resolution::UnresolvedValue,
|
|
||||||
snapshot::InstanceSnapshot,
|
|
||||||
syncback::{validate_file_name, SyncbackSnapshot},
|
|
||||||
RojoRef,
|
RojoRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,9 +36,6 @@ pub struct AdjacentMetadata {
|
|||||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||||
pub attributes: IndexMap<String, UnresolvedValue>,
|
pub attributes: IndexMap<String, UnresolvedValue>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -150,26 +144,6 @@ impl AdjacentMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = snapshot
|
|
||||||
.old_inst()
|
|
||||||
.and_then(|inst| inst.metadata().specified_name.clone())
|
|
||||||
.or_else(|| {
|
|
||||||
// Write name when name_for_inst would produce a different
|
|
||||||
// filesystem stem (slugification or init-prefix).
|
|
||||||
if snapshot.old_inst().is_none() {
|
|
||||||
let instance_name = &snapshot.new_inst().name;
|
|
||||||
if validate_file_name(instance_name).is_err()
|
|
||||||
|| instance_name.to_lowercase() == "init"
|
|
||||||
{
|
|
||||||
Some(instance_name.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
ignore_unknown_instances: if ignore_unknown_instances {
|
ignore_unknown_instances: if ignore_unknown_instances {
|
||||||
Some(true)
|
Some(true)
|
||||||
@@ -181,7 +155,6 @@ impl AdjacentMetadata {
|
|||||||
path,
|
path,
|
||||||
id: None,
|
id: None,
|
||||||
schema,
|
schema,
|
||||||
name,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,26 +213,11 @@ impl AdjacentMetadata {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_name(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
|
||||||
if self.name.is_some() && snapshot.metadata.specified_name.is_some() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"cannot specify a name using {} (instance has a name from somewhere else)",
|
|
||||||
self.path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(name) = &self.name {
|
|
||||||
snapshot.name = name.clone().into();
|
|
||||||
}
|
|
||||||
snapshot.metadata.specified_name = self.name.take();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
||||||
self.apply_ignore_unknown_instances(snapshot);
|
self.apply_ignore_unknown_instances(snapshot);
|
||||||
self.apply_properties(snapshot)?;
|
self.apply_properties(snapshot)?;
|
||||||
self.apply_id(snapshot)?;
|
self.apply_id(snapshot)?;
|
||||||
self.apply_schema(snapshot)?;
|
self.apply_schema(snapshot)?;
|
||||||
self.apply_name(snapshot)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,13 +226,11 @@ impl AdjacentMetadata {
|
|||||||
///
|
///
|
||||||
/// - The number of properties and attributes is 0
|
/// - The number of properties and attributes is 0
|
||||||
/// - `ignore_unknown_instances` is None
|
/// - `ignore_unknown_instances` is None
|
||||||
/// - `name` is None
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.attributes.is_empty()
|
self.attributes.is_empty()
|
||||||
&& self.properties.is_empty()
|
&& self.properties.is_empty()
|
||||||
&& self.ignore_unknown_instances.is_none()
|
&& self.ignore_unknown_instances.is_none()
|
||||||
&& self.name.is_none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add method to allow selectively applying parts of metadata and
|
// TODO: Add method to allow selectively applying parts of metadata and
|
||||||
@@ -306,9 +262,6 @@ pub struct DirectoryMetadata {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub class_name: Option<Ustr>,
|
pub class_name: Option<Ustr>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -419,26 +372,6 @@ impl DirectoryMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = snapshot
|
|
||||||
.old_inst()
|
|
||||||
.and_then(|inst| inst.metadata().specified_name.clone())
|
|
||||||
.or_else(|| {
|
|
||||||
// Write name when name_for_inst would produce a different
|
|
||||||
// directory name (slugification or init-prefix).
|
|
||||||
if snapshot.old_inst().is_none() {
|
|
||||||
let instance_name = &snapshot.new_inst().name;
|
|
||||||
if validate_file_name(instance_name).is_err()
|
|
||||||
|| instance_name.to_lowercase() == "init"
|
|
||||||
{
|
|
||||||
Some(instance_name.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
ignore_unknown_instances: if ignore_unknown_instances {
|
ignore_unknown_instances: if ignore_unknown_instances {
|
||||||
Some(true)
|
Some(true)
|
||||||
@@ -451,7 +384,6 @@ impl DirectoryMetadata {
|
|||||||
path,
|
path,
|
||||||
id: None,
|
id: None,
|
||||||
schema,
|
schema,
|
||||||
name,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +393,6 @@ impl DirectoryMetadata {
|
|||||||
self.apply_properties(snapshot)?;
|
self.apply_properties(snapshot)?;
|
||||||
self.apply_id(snapshot)?;
|
self.apply_id(snapshot)?;
|
||||||
self.apply_schema(snapshot)?;
|
self.apply_schema(snapshot)?;
|
||||||
self.apply_name(snapshot)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -533,33 +464,17 @@ impl DirectoryMetadata {
|
|||||||
snapshot.metadata.schema = self.schema.take();
|
snapshot.metadata.schema = self.schema.take();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_name(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
|
|
||||||
if self.name.is_some() && snapshot.metadata.specified_name.is_some() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"cannot specify a name using {} (instance has a name from somewhere else)",
|
|
||||||
self.path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(name) = &self.name {
|
|
||||||
snapshot.name = name.clone().into();
|
|
||||||
}
|
|
||||||
snapshot.metadata.specified_name = self.name.take();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Returns whether the metadata is 'empty', meaning it doesn't have anything
|
/// Returns whether the metadata is 'empty', meaning it doesn't have anything
|
||||||
/// worth persisting in it. Specifically:
|
/// worth persisting in it. Specifically:
|
||||||
///
|
///
|
||||||
/// - The number of properties and attributes is 0
|
/// - The number of properties and attributes is 0
|
||||||
/// - `ignore_unknown_instances` is None
|
/// - `ignore_unknown_instances` is None
|
||||||
/// - `class_name` is either None or not Some("Folder")
|
/// - `class_name` is either None or not Some("Folder")
|
||||||
/// - `name` is None
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.attributes.is_empty()
|
self.attributes.is_empty()
|
||||||
&& self.properties.is_empty()
|
&& self.properties.is_empty()
|
||||||
&& self.ignore_unknown_instances.is_none()
|
&& self.ignore_unknown_instances.is_none()
|
||||||
&& self.name.is_none()
|
|
||||||
&& if let Some(class) = &self.class_name {
|
&& if let Some(class) = &self.class_name {
|
||||||
class == "Folder"
|
class == "Folder"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub fn snapshot_rbxm(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub fn snapshot_rbxmx(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub fn snapshot_toml(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub fn snapshot_txt(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,14 +58,8 @@ pub fn syncback_txt<'sync>(
|
|||||||
|
|
||||||
if !meta.is_empty() {
|
if !meta.is_empty() {
|
||||||
let parent = snapshot.path.parent_err()?;
|
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(
|
fs_snapshot.add_file(
|
||||||
parent.join(format!("{meta_stem}.meta.json")),
|
parent.join(format!("{}.meta.json", new_inst.name)),
|
||||||
serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?,
|
serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub fn snapshot_yaml(
|
|||||||
.metadata(
|
.metadata(
|
||||||
InstanceMetadata::new()
|
InstanceMetadata::new()
|
||||||
.instigating_source(path)
|
.instigating_source(path)
|
||||||
.relevant_paths(vec![vfs.canonicalize(path)?])
|
.relevant_paths(vec![path.to_path_buf()])
|
||||||
.context(context),
|
.context(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use rbx_dom_weak::Instance;
|
|||||||
|
|
||||||
use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware};
|
use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware};
|
||||||
|
|
||||||
pub fn name_for_inst<'a>(
|
pub fn name_for_inst<'old>(
|
||||||
middleware: Middleware,
|
middleware: Middleware,
|
||||||
new_inst: &'a Instance,
|
new_inst: &Instance,
|
||||||
old_inst: Option<InstanceWithMeta<'a>>,
|
old_inst: Option<InstanceWithMeta<'old>>,
|
||||||
) -> anyhow::Result<Cow<'a, str>> {
|
) -> anyhow::Result<Cow<'old, str>> {
|
||||||
if let Some(old_inst) = old_inst {
|
if let Some(old_inst) = old_inst {
|
||||||
if let Some(source) = old_inst.metadata().relevant_paths.first() {
|
if let Some(source) = old_inst.metadata().relevant_paths.first() {
|
||||||
source
|
source
|
||||||
@@ -35,34 +35,14 @@ pub fn name_for_inst<'a>(
|
|||||||
| Middleware::CsvDir
|
| Middleware::CsvDir
|
||||||
| Middleware::ServerScriptDir
|
| Middleware::ServerScriptDir
|
||||||
| Middleware::ClientScriptDir
|
| Middleware::ClientScriptDir
|
||||||
| Middleware::ModuleScriptDir => {
|
| Middleware::ModuleScriptDir => Cow::Owned(new_inst.name.clone()),
|
||||||
let name = if validate_file_name(&new_inst.name).is_err() {
|
|
||||||
Cow::Owned(slugify_name(&new_inst.name))
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(new_inst.name.as_str())
|
|
||||||
};
|
|
||||||
// Prefix "init" to avoid colliding with reserved init files.
|
|
||||||
if name.to_lowercase() == "init" {
|
|
||||||
Cow::Owned(format!("_{name}"))
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
let extension = extension_for_middleware(middleware);
|
let extension = extension_for_middleware(middleware);
|
||||||
let slugified;
|
let name = &new_inst.name;
|
||||||
let stem: &str = if validate_file_name(&new_inst.name).is_err() {
|
validate_file_name(name).with_context(|| {
|
||||||
slugified = slugify_name(&new_inst.name);
|
format!("name '{name}' is not legal to write to the file system")
|
||||||
&slugified
|
})?;
|
||||||
} else {
|
Cow::Owned(format!("{name}.{extension}"))
|
||||||
&new_inst.name
|
|
||||||
};
|
|
||||||
// Prefix "init" stems to avoid colliding with reserved init files.
|
|
||||||
if stem.to_lowercase() == "init" {
|
|
||||||
Cow::Owned(format!("_{stem}.{extension}"))
|
|
||||||
} else {
|
|
||||||
Cow::Owned(format!("{stem}.{extension}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -114,39 +94,6 @@ const INVALID_WINDOWS_NAMES: [&str; 22] = [
|
|||||||
/// in a file's name.
|
/// in a file's name.
|
||||||
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
||||||
|
|
||||||
/// Slugifies a name by replacing forbidden characters with underscores
|
|
||||||
/// and ensuring the result is a valid file name
|
|
||||||
pub fn slugify_name(name: &str) -> String {
|
|
||||||
let mut result = String::with_capacity(name.len());
|
|
||||||
|
|
||||||
for ch in name.chars() {
|
|
||||||
if FORBIDDEN_CHARS.contains(&ch) {
|
|
||||||
result.push('_');
|
|
||||||
} else {
|
|
||||||
result.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Windows reserved names by appending an underscore
|
|
||||||
let result_lower = result.to_lowercase();
|
|
||||||
for forbidden in INVALID_WINDOWS_NAMES {
|
|
||||||
if result_lower == forbidden.to_lowercase() {
|
|
||||||
result.push('_');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while result.ends_with(' ') || result.ends_with('.') {
|
|
||||||
result.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.is_empty() || result.chars().all(|c| c == '_') {
|
|
||||||
result = "instance".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates a provided file name to ensure it's allowed on the file system. An
|
/// Validates a provided file name to ensure it's allowed on the file system. An
|
||||||
/// error is returned if the name isn't allowed, indicating why.
|
/// error is returned if the name isn't allowed, indicating why.
|
||||||
/// This takes into account rules for Windows, MacOS, and Linux.
|
/// This takes into account rules for Windows, MacOS, and Linux.
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ pub fn syncback_loop(
|
|||||||
old_tree: &mut RojoTree,
|
old_tree: &mut RojoTree,
|
||||||
mut new_tree: WeakDom,
|
mut new_tree: WeakDom,
|
||||||
project: &Project,
|
project: &Project,
|
||||||
|
force_json: bool,
|
||||||
) -> anyhow::Result<FsSnapshot> {
|
) -> anyhow::Result<FsSnapshot> {
|
||||||
let ignore_patterns = project
|
let ignore_patterns = project
|
||||||
.syncback_rules
|
.syncback_rules
|
||||||
@@ -153,6 +154,7 @@ pub fn syncback_loop(
|
|||||||
old_tree,
|
old_tree,
|
||||||
new_tree: &new_tree,
|
new_tree: &new_tree,
|
||||||
project,
|
project,
|
||||||
|
force_json,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut snapshots = vec![SyncbackSnapshot {
|
let mut snapshots = vec![SyncbackSnapshot {
|
||||||
@@ -197,7 +199,7 @@ pub fn syncback_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let middleware = get_best_middleware(&snapshot);
|
let middleware = get_best_middleware(&snapshot, force_json);
|
||||||
|
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"Middleware for {inst_path} is {:?} (path is {})",
|
"Middleware for {inst_path} is {:?} (path is {})",
|
||||||
@@ -213,10 +215,14 @@ pub fn syncback_loop(
|
|||||||
let syncback = match middleware.syncback(&snapshot) {
|
let syncback = match middleware.syncback(&snapshot) {
|
||||||
Ok(syncback) => syncback,
|
Ok(syncback) => syncback,
|
||||||
Err(err) if middleware == Middleware::Dir => {
|
Err(err) if middleware == Middleware::Dir => {
|
||||||
let new_middleware = match env::var(DEBUG_MODEL_FORMAT_VAR) {
|
let new_middleware = if force_json {
|
||||||
Ok(value) if value == "1" => Middleware::Rbxmx,
|
Middleware::JsonModel
|
||||||
Ok(value) if value == "2" => Middleware::JsonModel,
|
} else {
|
||||||
_ => Middleware::Rbxm,
|
match env::var(DEBUG_MODEL_FORMAT_VAR) {
|
||||||
|
Ok(value) if value == "1" => Middleware::Rbxmx,
|
||||||
|
Ok(value) if value == "2" => Middleware::JsonModel,
|
||||||
|
_ => Middleware::Rbxm,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let file_name = snapshot
|
let file_name = snapshot
|
||||||
.path
|
.path
|
||||||
@@ -295,7 +301,7 @@ pub struct SyncbackReturn<'sync> {
|
|||||||
pub removed_children: Vec<InstanceWithMeta<'sync>>,
|
pub removed_children: Vec<InstanceWithMeta<'sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware {
|
pub fn get_best_middleware(snapshot: &SyncbackSnapshot, force_json: bool) -> Middleware {
|
||||||
// At some point, we're better off using an O(1) method for checking
|
// At some point, we're better off using an O(1) method for checking
|
||||||
// equality for classes like this.
|
// equality for classes like this.
|
||||||
static JSON_MODEL_CLASSES: OnceLock<HashSet<&str>> = OnceLock::new();
|
static JSON_MODEL_CLASSES: OnceLock<HashSet<&str>> = OnceLock::new();
|
||||||
@@ -361,10 +367,18 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if middleware == Middleware::Rbxm {
|
if middleware == Middleware::Rbxm {
|
||||||
middleware = match env::var(DEBUG_MODEL_FORMAT_VAR) {
|
middleware = if force_json {
|
||||||
Ok(value) if value == "1" => Middleware::Rbxmx,
|
if !inst.children().is_empty() {
|
||||||
Ok(value) if value == "2" => Middleware::JsonModel,
|
Middleware::Dir
|
||||||
_ => Middleware::Rbxm,
|
} else {
|
||||||
|
Middleware::JsonModel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match env::var(DEBUG_MODEL_FORMAT_VAR) {
|
||||||
|
Ok(value) if value == "1" => Middleware::Rbxmx,
|
||||||
|
Ok(value) if value == "2" => Middleware::JsonModel,
|
||||||
|
_ => Middleware::Rbxm,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub struct SyncbackData<'sync> {
|
|||||||
pub(super) old_tree: &'sync RojoTree,
|
pub(super) old_tree: &'sync RojoTree,
|
||||||
pub(super) new_tree: &'sync WeakDom,
|
pub(super) new_tree: &'sync WeakDom,
|
||||||
pub(super) project: &'sync Project,
|
pub(super) project: &'sync Project,
|
||||||
|
pub(super) force_json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SyncbackSnapshot<'sync> {
|
pub struct SyncbackSnapshot<'sync> {
|
||||||
@@ -31,25 +32,6 @@ pub struct SyncbackSnapshot<'sync> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'sync> SyncbackSnapshot<'sync> {
|
impl<'sync> SyncbackSnapshot<'sync> {
|
||||||
/// Computes the middleware and filesystem name for a child without
|
|
||||||
/// creating a full snapshot. Uses the same logic as `with_joined_path`.
|
|
||||||
pub fn child_middleware_and_name(
|
|
||||||
&self,
|
|
||||||
new_ref: Ref,
|
|
||||||
old_ref: Option<Ref>,
|
|
||||||
) -> anyhow::Result<(Middleware, String)> {
|
|
||||||
let temp = Self {
|
|
||||||
data: self.data,
|
|
||||||
old: old_ref,
|
|
||||||
new: new_ref,
|
|
||||||
path: PathBuf::new(),
|
|
||||||
middleware: None,
|
|
||||||
};
|
|
||||||
let middleware = get_best_middleware(&temp, self.data.force_json);
|
|
||||||
let name = name_for_inst(middleware, temp.new_inst(), temp.old_inst())?;
|
|
||||||
Ok((middleware, name.into_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a SyncbackSnapshot from the provided refs
|
/// Constructs a SyncbackSnapshot from the provided refs
|
||||||
/// while inheriting this snapshot's path and data. This should be used for
|
/// while inheriting this snapshot's path and data. This should be used for
|
||||||
/// directories.
|
/// directories.
|
||||||
@@ -62,7 +44,7 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||||||
path: PathBuf::new(),
|
path: PathBuf::new(),
|
||||||
middleware: None,
|
middleware: None,
|
||||||
};
|
};
|
||||||
let middleware = get_best_middleware(&snapshot);
|
let middleware = get_best_middleware(&snapshot, self.data.force_json);
|
||||||
let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
|
let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
|
||||||
snapshot.path = self.path.join(name.as_ref());
|
snapshot.path = self.path.join(name.as_ref());
|
||||||
|
|
||||||
@@ -88,7 +70,7 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||||||
path: PathBuf::new(),
|
path: PathBuf::new(),
|
||||||
middleware: None,
|
middleware: None,
|
||||||
};
|
};
|
||||||
let middleware = get_best_middleware(&snapshot);
|
let middleware = get_best_middleware(&snapshot, self.data.force_json);
|
||||||
let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
|
let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
|
||||||
snapshot.path = base_path.join(name.as_ref());
|
snapshot.path = base_path.join(name.as_ref());
|
||||||
|
|
||||||
@@ -256,25 +238,6 @@ pub fn inst_path(dom: &WeakDom, referent: Ref) -> String {
|
|||||||
path.join("/")
|
path.join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'sync> SyncbackData<'sync> {
|
|
||||||
/// Constructs a `SyncbackData` for use in unit tests.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn for_test(
|
|
||||||
vfs: &'sync Vfs,
|
|
||||||
old_tree: &'sync RojoTree,
|
|
||||||
new_tree: &'sync WeakDom,
|
|
||||||
project: &'sync Project,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
vfs,
|
|
||||||
old_tree,
|
|
||||||
new_tree,
|
|
||||||
project,
|
|
||||||
force_json: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
use rbx_dom_weak::{InstanceBuilder, WeakDom};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ gen_build_tests! {
|
|||||||
issue_546,
|
issue_546,
|
||||||
json_as_lua,
|
json_as_lua,
|
||||||
json_model_in_folder,
|
json_model_in_folder,
|
||||||
|
json_model_legacy_name,
|
||||||
module_in_folder,
|
module_in_folder,
|
||||||
module_init,
|
module_init,
|
||||||
nested_runcontext,
|
nested_runcontext,
|
||||||
@@ -54,8 +55,6 @@ gen_build_tests! {
|
|||||||
script_meta_disabled,
|
script_meta_disabled,
|
||||||
server_in_folder,
|
server_in_folder,
|
||||||
server_init,
|
server_init,
|
||||||
slugified_name_roundtrip,
|
|
||||||
model_json_name_input,
|
|
||||||
txt,
|
txt,
|
||||||
txt_in_folder,
|
txt_in_folder,
|
||||||
unresolved_values,
|
unresolved_values,
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ syncback_tests! {
|
|||||||
// Ensures that projects can be reserialized by syncback and that
|
// Ensures that projects can be reserialized by syncback and that
|
||||||
// default.project.json doesn't change unexpectedly.
|
// default.project.json doesn't change unexpectedly.
|
||||||
project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json"],
|
project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json"],
|
||||||
// Confirms that duplicate children are resolved with incrementing suffixes
|
// Confirms that Instances that cannot serialize as directories serialize as rbxms
|
||||||
rbxm_fallback => ["src/ChildWithDuplicates/DuplicateChild/.gitkeep", "src/ChildWithDuplicates/DuplicateChild1/.gitkeep"],
|
rbxm_fallback => ["src/ChildWithDuplicates.rbxm"],
|
||||||
// Ensures that ref properties are linked properly on the file system
|
// Ensures that ref properties are linked properly on the file system
|
||||||
ref_properties => ["src/pointer.model.json", "src/target.model.json"],
|
ref_properties => ["src/pointer.model.json", "src/target.model.json"],
|
||||||
// Ensures that ref properties are linked when no attributes are manually
|
// Ensures that ref properties are linked when no attributes are manually
|
||||||
@@ -86,9 +86,4 @@ syncback_tests! {
|
|||||||
sync_rules => ["src/module.modulescript", "src/text.text"],
|
sync_rules => ["src/module.modulescript", "src/text.text"],
|
||||||
// Ensures that the `syncUnscriptable` setting works
|
// Ensures that the `syncUnscriptable` setting works
|
||||||
unscriptable_properties => ["default.project.json"],
|
unscriptable_properties => ["default.project.json"],
|
||||||
// Ensures that instances with names containing illegal characters get slugified filenames
|
|
||||||
// and preserve their original names in meta.json without forcing directories for leaf scripts
|
|
||||||
slugified_name => ["src/_Script.meta.json", "src/_Script.server.luau", "src/_Folder/init.meta.json"],
|
|
||||||
// Ensures that .model.json files preserve the name property
|
|
||||||
model_json_name => ["src/foo.model.json"],
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user