From 9b5a07191b5bfeb66d60ce478cf34e9c7a5a9793 Mon Sep 17 00:00:00 2001 From: Micah Date: Wed, 19 Nov 2025 09:21:33 -0800 Subject: [PATCH] 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. --- CHANGELOG.md | 37 ++ Cargo.lock | 32 +- Cargo.toml | 6 +- crates/memofs/CHANGELOG.md | 1 + crates/memofs/src/in_memory_fs.rs | 15 + crates/memofs/src/lib.rs | 62 ++ crates/memofs/src/noop_backend.rs | 14 + crates/memofs/src/std_backend.rs | 8 + ...__syncback_util__child_but_not-stdout.snap | 6 + ..._rojo_test__syncback_util__csv-stdout.snap | 7 + ...ncback_util__duplicate_rojo_id-stdout.snap | 5 + ...back_util__ignore_paths_adding-stdout.snap | 7 + ...ncback_util__ignore_paths_init-stdout.snap | 6 + ...nore_paths_removing-src__Message.rbxm.snap | 73 +++ ...ck_util__ignore_paths_removing-stdout.snap | 5 + ...back_util__ignore_trees_adding-stdout.snap | 6 + ...ck_util__ignore_trees_removing-stdout.snap | 5 + ...yncback_util__json_middlewares-stdout.snap | 8 + ...syncback_util__nested_projects-stdout.snap | 6 + ...ck_util__nested_projects_weird-stdout.snap | 6 + ...t__syncback_util__project_init-stdout.snap | 6 + ...back_util__project_reserialize-stdout.snap | 6 + ...allback-src__ChildWithDuplicates.rbxm.snap | 73 +++ ...__syncback_util__rbxm_fallback-stdout.snap | 9 + ..._syncback_util__ref_properties-stdout.snap | 6 + ...ack_util__ref_properties_blank-stdout.snap | 7 + ..._util__ref_properties_conflict-stdout.snap | 6 + ...util__ref_properties_duplicate-stdout.snap | 5 + ...respect_old_middleware-src__rbxm.rbxm.snap | 55 ++ ...k_util__respect_old_middleware-stdout.snap | 8 + ...ack_util__string_value_project-stdout.snap | 6 + ...est__syncback_util__sync_rules-stdout.snap | 6 + ..._util__unscriptable_properties-stdout.snap | 5 + ...ut_not-OnlyOneCopy__child_of_one.luau.snap | 5 + ...torage__child_replicated_storage.luau.snap | 5 + ...nd__tests__syncback__csv-src__csv.csv.snap | 6 + ...syncback__csv-src__csv_init__init.csv.snap | 6 + ...uplicate_rojo_id-container.model.json.snap | 25 + ...aths_adding-src__int_value.model.json.snap | 10 + ...ding-src__subfolder__string_value.txt.snap | 5 + ..._paths_init-src__init-file__init.luau.snap | 5 + ..._ignore_paths_init-src__non-init.luau.snap | 6 + ...es-src__dir_with_meta__init.meta.json.snap | 12 + ...iddlewares-src__model_json.model.json.snap | 10 + ...ewares-src__project_json.project.json.snap | 17 + ...__nested_projects-nested.project.json.snap | 19 + ...ack__nested_projects-string_value.txt.snap | 5 + ...weird-src__modules__ClientModule.luau.snap | 5 + ...weird-src__modules__ServerModule.luau.snap | 5 + ...syncback__project_init-src__init.luau.snap | 5 + ...t_reserialize-attribute_mismatch.luau.snap | 5 + ...ialize-property_mismatch.project.json.snap | 15 + ...ef_properties-src__pointer.model.json.snap | 10 + ...ref_properties-src__target.model.json.snap | 10 + ...perties_blank-src__pointer.model.json.snap | 10 + ...roperties_blank-src__target.meta.json.snap | 9 + ..._ref_properties_blank-src__target.txt.snap | 5 + ...es_conflict-src__Pointer_2.model.json.snap | 11 + ...ies_conflict-src__Target_2.model.json.snap | 11 + ...t_old_middleware-default.project.json.snap | 16 + ...middleware-src__model_json.model.json.snap | 10 + ...spect_old_middleware-src__rbxmx.rbxmx.snap | 16 + ...ng_value_project-default.project.json.snap | 22 + ...__sync_rules-src__module.modulescript.snap | 6 + ...__syncback__sync_rules-src__text.text.snap | 6 + ...table_properties-default.project.json.snap | 18 + .../input-project/OnlyOneCopy/.gitkeep | 0 .../input-project/ReplicatedStorage/.gitkeep | 0 .../input-project/default.project.json | 12 + .../syncback-tests/child_but_not/input.rbxl | Bin 0 -> 56708 bytes .../csv/input-project/default.project.json | 6 + .../csv/input-project/src/csv_init/init.csv | 2 + rojo-test/syncback-tests/csv/input.rbxm | Bin 0 -> 1041 bytes .../input-project/container.model.json | 13 + .../input-project/default.project.json | 11 + .../duplicate_rojo_id/input.rbxl | Bin 0 -> 53809 bytes .../input-project/default.project.json | 11 + .../input-project/src/.gitkeep | 0 .../ignore_paths_adding/input.rbxm | Bin 0 -> 3670 bytes .../input-project/default.project.json | 11 + .../input-project/src/init-file/init.luau | 1 + .../input-project/src/non-init.luau | 1 + .../ignore_paths_init/input.rbxm | Bin 0 -> 1111 bytes .../input-project/default.project.json | 14 + .../input-project/src/not_in_place.luau | 1 + .../ignore_paths_removing/input.rbxl | Bin 0 -> 45880 bytes .../input-project/default.project.json | 14 + .../input-project/src/.gitkeep | 0 .../ignore_trees_adding/input.rbxl | Bin 0 -> 53275 bytes .../input-project/default.project.json | 14 + .../input-project/src/KeepMe/.gitkeep | 0 .../ignore_trees_removing/input.rbxl | Bin 0 -> 43090 bytes .../input-project/default.project.json | 6 + .../src/dir_with_meta/init.meta.json | 3 + .../input-project/src/model_json.model.json | 3 + .../src/project_json.project.json | 6 + .../json_middlewares/input.rbxm | Bin 0 -> 1551 bytes .../expected/default.project.json | 11 + .../expected/nested.project.json | 15 + .../nested_projects/expected/string_value.txt | 1 + .../input-project/default.project.json | 11 + .../input-project/nested.project.json | 12 + .../input-project/string_value.txt | 0 .../syncback-tests/nested_projects/input.rbxl | Bin 0 -> 56532 bytes .../input-project/client-only.project.json | 9 + .../input-project/default.project.json | 16 + .../input-project/server-only.project.json | 9 + .../input-project/src/modules/.gitkeep | 0 .../nested_projects_weird/input.rbxl | Bin 0 -> 56762 bytes .../input-project/default.project.json | 11 + .../project_init/input-project/src/init.luau | 0 .../syncback-tests/project_init/input.rbxl | Bin 0 -> 56195 bytes .../expected/attribute_mismatch.luau | 1 + .../expected/default.project.json | 29 + .../expected/property_mismatch.project.json | 11 + .../input-project/attribute_mismatch.luau | 0 .../input-project/default.project.json | 15 + .../property_mismatch.project.json | 6 + .../project_reserialize/input.rbxm | Bin 0 -> 1381 bytes .../input-project/default.project.json | 6 + .../src/ChildWithDuplicates/.gitkeep | 0 .../syncback-tests/rbxm_fallback/input.rbxm | Bin 0 -> 549 bytes .../input-project/default.project.json | 9 + .../input-project/src/pointer.model.json | 3 + .../input-project/src/target.model.json | 3 + .../syncback-tests/ref_properties/input.rbxl | Bin 0 -> 56493 bytes .../input-project/default.project.json | 9 + .../input-project/src/.gitkeep | 0 .../ref_properties_blank/input.rbxl | Bin 0 -> 49473 bytes .../input-project/default.project.json | 9 + .../input-project/src/Pointer_1.model.json | 6 + .../input-project/src/Pointer_2.model.json | 6 + .../input-project/src/Target_1.model.json | 6 + .../input-project/src/Target_2.model.json | 6 + .../ref_properties_conflict/input.rbxl | Bin 0 -> 66581 bytes .../input-project/default.project.json | 6 + .../input-project/src/Pointer_1.model.json | 6 + .../input-project/src/Pointer_2.model.json | 6 + .../input-project/src/Target.model.json | 6 + .../ref_properties_duplicate/input.rbxm | Bin 0 -> 990 bytes .../input-project/default.project.json | 9 + .../input-project/src/model_json.model.json | 3 + .../input-project/src/rbxm.rbxm | Bin 0 -> 446 bytes .../input-project/src/rbxmx.rbxmx | 15 + .../respect_old_middleware/input.rbxm | Bin 0 -> 1254 bytes .../input-project/default.project.json | 12 + .../input-project/string_value.txt | 0 .../string_value_project/input.rbxm | Bin 0 -> 1033 bytes .../input-project/default.project.json | 16 + .../input-project/src/module.modulescript | 1 + .../sync_rules/input-project/src/text.text | 1 + .../syncback-tests/sync_rules/input.rbxm | Bin 0 -> 1487 bytes .../input-project/default.project.json | 9 + .../unscriptable_properties/input.rbxm | Bin 0 -> 503 bytes src/change_processor.rs | 17 +- src/cli/mod.rs | 4 + src/cli/syncback.rs | 282 +++++++++ src/lib.rs | 10 + src/path_serializer.rs | 22 +- src/project.rs | 29 +- src/resolution.rs | 96 +++- src/serve_session.rs | 4 + src/snapshot/metadata.rs | 44 +- ..._snapshot__tests__apply__add_property.snap | 1 + ...s__apply__remove_property_after_patch.snap | 1 + ...tests__apply__remove_property_initial.snap | 1 + ...tests__apply__set_name_and_class_name.snap | 1 + ...__snapshot__tests__compute__add_child.snap | 1 + src/snapshot/tree.rs | 19 + src/snapshot_middleware/csv.rs | 180 +++++- src/snapshot_middleware/dir.rs | 170 +++++- src/snapshot_middleware/json_model.rs | 95 +++- src/snapshot_middleware/lua.rs | 99 +++- src/snapshot_middleware/meta_file.rs | 246 +++++++- src/snapshot_middleware/mod.rs | 286 +++++++--- src/snapshot_middleware/project.rs | 342 ++++++++++- src/snapshot_middleware/rbxm.rs | 23 +- src/snapshot_middleware/rbxmx.rs | 34 +- ...t_middleware__csv__test__csv_from_vfs.snap | 1 + ...pshot_middleware__csv__test__csv_init.snap | 1 + ...leware__csv__test__csv_init_with_meta.snap | 1 + ..._middleware__csv__test__csv_with_meta.snap | 1 + ...t_middleware__dir__test__empty_folder.snap | 1 + ...ddleware__dir__test__folder_in_folder.snap | 2 + ...leware__json__test__instance_from_vfs.snap | 1 + ...middleware__json__test__with_metadata.snap | 1 + ...are__json_model__test__model_from_vfs.snap | 2 + ...on_model__test__model_from_vfs_legacy.snap | 2 + ...are__lua__test__class_client_from_vfs.snap | 1 + ...are__lua__test__class_module_from_vfs.snap | 1 + ...re__lua__test__class_module_with_meta.snap | 1 + ...are__lua__test__class_script_disabled.snap | 1 + ...re__lua__test__class_script_with_meta.snap | 1 + ...are__lua__test__class_server_from_vfs.snap | 1 + ...ware__lua__test__init_module_from_vfs.snap | 1 + ..._test__init_module_from_vfs_with_meta.snap | 1 + ...re__lua__test__plugin_module_from_vfs.snap | 1 + ...lua__test__runcontext_client_from_vfs.snap | 1 + ...lua__test__runcontext_module_from_vfs.snap | 1 + ...ua__test__runcontext_module_with_meta.snap | 1 + ...lua__test__runcontext_script_disabled.snap | 1 + ...ua__test__runcontext_script_with_meta.snap | 1 + ...lua__test__runcontext_server_from_vfs.snap | 1 + ...__meta_file__test__adjacent_read_json.snap | 1 + ..._meta_file__test__adjacent_read_jsonc.snap | 1 + ..._meta_file__test__directory_read_json.snap | 1 + ...meta_file__test__directory_read_jsonc.snap | 1 + ...eware__project__test__no_name_project.snap | 1 + ...oject__test__project_from_direct_file.snap | 1 + ...test__project_path_property_overrides.snap | 1 + ..._project__test__project_with_children.snap | 11 +- ...t__test__project_with_path_to_project.snap | 1 + ...ct_with_path_to_project_with_children.snap | 11 +- ...oject__test__project_with_path_to_txt.snap | 1 + ...est__project_with_resolved_properties.snap | 1 + ...t__project_with_unresolved_properties.snap | 1 + ...leware__toml__test__instance_from_vfs.snap | 1 + ...middleware__toml__test__with_metadata.snap | 1 + ...dleware__txt__test__instance_from_vfs.snap | 1 + ..._middleware__txt__test__with_metadata.snap | 1 + ...leware__yaml__test__instance_from_vfs.snap | 1 + ...middleware__yaml__test__with_metadata.snap | 1 + src/snapshot_middleware/txt.rs | 44 +- src/snapshot_middleware/util.rs | 7 + src/syncback/file_names.rs | 128 +++++ src/syncback/fs_snapshot.rs | 191 +++++++ src/syncback/hash/mod.rs | 122 ++++ src/syncback/hash/variant.rs | 212 +++++++ src/syncback/mod.rs | 534 ++++++++++++++++++ src/syncback/property_filter.rs | 111 ++++ src/syncback/ref_properties.rs | 192 +++++++ src/syncback/snapshot.rs | 259 +++++++++ src/variant_eq.rs | 191 +++++++ src/web/ui.rs | 1 + tests/rojo_test/io_util.rs | 2 + tests/rojo_test/mod.rs | 1 + tests/rojo_test/syncback_util.rs | 116 ++++ tests/tests/mod.rs | 1 + tests/tests/syncback.rs | 81 +++ 239 files changed, 5325 insertions(+), 225 deletions(-) create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap create mode 100644 rojo-test/syncback-tests/child_but_not/input-project/OnlyOneCopy/.gitkeep create mode 100644 rojo-test/syncback-tests/child_but_not/input-project/ReplicatedStorage/.gitkeep create mode 100644 rojo-test/syncback-tests/child_but_not/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/child_but_not/input.rbxl create mode 100644 rojo-test/syncback-tests/csv/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv create mode 100644 rojo-test/syncback-tests/csv/input.rbxm create mode 100644 rojo-test/syncback-tests/duplicate_rojo_id/input-project/container.model.json create mode 100644 rojo-test/syncback-tests/duplicate_rojo_id/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl create mode 100644 rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_paths_adding/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ignore_paths_adding/input.rbxm create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau create mode 100644 rojo-test/syncback-tests/ignore_paths_init/input.rbxm create mode 100644 rojo-test/syncback-tests/ignore_paths_removing/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_paths_removing/input-project/src/not_in_place.luau create mode 100644 rojo-test/syncback-tests/ignore_paths_removing/input.rbxl create mode 100644 rojo-test/syncback-tests/ignore_trees_adding/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_trees_adding/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ignore_trees_adding/input.rbxl create mode 100644 rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ignore_trees_removing/input-project/src/KeepMe/.gitkeep create mode 100644 rojo-test/syncback-tests/ignore_trees_removing/input.rbxl create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json create mode 100644 rojo-test/syncback-tests/json_middlewares/input.rbxm create mode 100644 rojo-test/syncback-tests/nested_projects/expected/default.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/expected/nested.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/expected/string_value.txt create mode 100644 rojo-test/syncback-tests/nested_projects/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/input-project/nested.project.json create mode 100644 rojo-test/syncback-tests/nested_projects/input-project/string_value.txt create mode 100644 rojo-test/syncback-tests/nested_projects/input.rbxl create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/client-only.project.json create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/server-only.project.json create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input-project/src/modules/.gitkeep create mode 100644 rojo-test/syncback-tests/nested_projects_weird/input.rbxl create mode 100644 rojo-test/syncback-tests/project_init/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/project_init/input-project/src/init.luau create mode 100644 rojo-test/syncback-tests/project_init/input.rbxl create mode 100644 rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau create mode 100644 rojo-test/syncback-tests/project_reserialize/expected/default.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/input-project/attribute_mismatch.luau create mode 100644 rojo-test/syncback-tests/project_reserialize/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/input-project/property_mismatch.project.json create mode 100644 rojo-test/syncback-tests/project_reserialize/input.rbxm create mode 100644 rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/rbxm_fallback/input-project/src/ChildWithDuplicates/.gitkeep create mode 100644 rojo-test/syncback-tests/rbxm_fallback/input.rbxm create mode 100644 rojo-test/syncback-tests/ref_properties/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties/input-project/src/pointer.model.json create mode 100644 rojo-test/syncback-tests/ref_properties/input-project/src/target.model.json create mode 100644 rojo-test/syncback-tests/ref_properties/input.rbxl create mode 100644 rojo-test/syncback-tests/ref_properties_blank/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_blank/input-project/src/.gitkeep create mode 100644 rojo-test/syncback-tests/ref_properties_blank/input.rbxl create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_conflict/input.rbxl create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json create mode 100644 rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxmx.rbxmx create mode 100644 rojo-test/syncback-tests/respect_old_middleware/input.rbxm create mode 100644 rojo-test/syncback-tests/string_value_project/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/string_value_project/input-project/string_value.txt create mode 100644 rojo-test/syncback-tests/string_value_project/input.rbxm create mode 100644 rojo-test/syncback-tests/sync_rules/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript create mode 100644 rojo-test/syncback-tests/sync_rules/input-project/src/text.text create mode 100644 rojo-test/syncback-tests/sync_rules/input.rbxm create mode 100644 rojo-test/syncback-tests/unscriptable_properties/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/unscriptable_properties/input.rbxm create mode 100644 src/cli/syncback.rs create mode 100644 src/syncback/file_names.rs create mode 100644 src/syncback/fs_snapshot.rs create mode 100644 src/syncback/hash/mod.rs create mode 100644 src/syncback/hash/variant.rs create mode 100644 src/syncback/mod.rs create mode 100644 src/syncback/property_filter.rs create mode 100644 src/syncback/ref_properties.rs create mode 100644 src/syncback/snapshot.rs create mode 100644 src/variant_eq.rs create mode 100644 tests/rojo_test/syncback_util.rs create mode 100644 tests/tests/syncback.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6581dff8..b267ff8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,46 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* A new command `rojo syncback` has been added. It can be used as `rojo syncback [path to project] --input [path to file]`. ([#937]) + This command takes a Roblox file and pulls Instances out of it and places them in the correct position in the provided project. + Syncback is primarily controlled by the project file. Any Instances who are either referenced in the project file or a descendant + of one that is will be placed in an appropriate location. + + In addition, a new field has been added to project files, `syncbackRules` to control how it behaves: + + ```json + { + "syncbackRules": { + "ignoreTrees": [ + "ServerStorage/ImportantSecrets", + ], + "ignorePaths": [ + "src/ServerStorage/Secrets/*" + ], + "ignoreProperties": { + "BasePart": ["Color"] + }, + "syncCurrentCamera": false, + "syncUnscriptable": true, + } + } + ``` + + A brief explanation of each field: + + - `ignoreTrees` is a list of paths in the **roblox file** that should be ignored + - `ignorePaths` is a list of paths in the **file system** that should be ignored + - `ignoreProperties` is a list of properties that won't be synced back + - `syncCurrentCamera` is a toggle for whether to sync back the Workspace's CurrentCamera. Defaults to `false`. + - `syncUnscriptable` is a toggle for whether to sync back properties that cannot be set by the Roblox Studio plugin. Defaults to `true`. + + If you are used to the `UpliftGames` version of this feature, there are a few notable differences: + - `syncUnscriptable` defaults to `true` instead of `false` + - `ignoreTrees` doesn't require the root of the project's name in it. * Fixed bugs and improved performance & UX for the script diff viewer ([#994]) * Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159]) +[#937]: https://github.com/rojo-rbx/rojo/pull/937 [#994]: https://github.com/rojo-rbx/rojo/pull/994 [#1159]: https://github.com/rojo-rbx/rojo/pull/1159 diff --git a/Cargo.lock b/Cargo.lock index 46b973f7..52577619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -546,6 +546,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -765,7 +774,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.5", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -784,12 +793,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" - [[package]] name = "hashbrown" version = "0.15.4" @@ -935,12 +938,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.15.4", + "serde", ] [[package]] @@ -1635,6 +1639,7 @@ dependencies = [ "rbx_dom_weak", "rbx_reflection", "rbx_reflection_database", + "serde", "thiserror", "zstd", ] @@ -1898,6 +1903,7 @@ dependencies = [ "anyhow", "backtrace", "bincode", + "blake3", "clap 3.2.25", "criterion", "crossbeam-channel", @@ -1905,11 +1911,13 @@ dependencies = [ "data-encoding", "embed-resource", "env_logger", + "float-cmp", "fs-err", "futures", "globset", "humantime", "hyper", + "indexmap 2.10.0", "insta", "jod-thread", "jsonc-parser", @@ -2962,9 +2970,9 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce2a4ff45552406d02501cea6c18d8a7e50228e7736a872951fe2fe75c91be7" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", diff --git a/Cargo.toml b/Cargo.toml index 4a3b80bf..2deb184b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ memofs = { version = "0.3.0", path = "crates/memofs" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_xml = { path = "../rbx-dom/rbx_xml" } -rbx_binary = "2.0.0" +rbx_binary = { version = "2.0.0", features = ["unstable_text_format"] } rbx_dom_weak = "4.0.0" rbx_reflection = "6.0.0" rbx_reflection_database = "2.0.1" @@ -97,6 +97,10 @@ profiling = "1.0.15" yaml-rust2 = "0.10.3" data-encoding = "2.8.0" +blake3 = "1.5.0" +float-cmp = "0.9.0" +indexmap = { version = "2.10.0", features = ["serde"] } + [target.'cfg(windows)'.dependencies] winreg = "0.10.1" diff --git a/crates/memofs/CHANGELOG.md b/crates/memofs/CHANGELOG.md index 44c3aa50..16d578c3 100644 --- a/crates/memofs/CHANGELOG.md +++ b/crates/memofs/CHANGELOG.md @@ -5,6 +5,7 @@ ## 0.3.0 (2024-03-15) * Changed `StdBackend` file watching component to use minimal recursive watches. [#830] * Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854] +* Added `create_dir` and `create_dir_all` to allow creating directories. [#830]: https://github.com/rojo-rbx/rojo/pull/830 [#854]: https://github.com/rojo-rbx/rojo/pull/854 diff --git a/crates/memofs/src/in_memory_fs.rs b/crates/memofs/src/in_memory_fs.rs index 7bc654af..acca08c4 100644 --- a/crates/memofs/src/in_memory_fs.rs +++ b/crates/memofs/src/in_memory_fs.rs @@ -176,6 +176,21 @@ impl VfsBackend for InMemoryFs { } } + fn create_dir(&mut self, path: &Path) -> io::Result<()> { + let mut inner = self.inner.lock().unwrap(); + inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir()) + } + + fn create_dir_all(&mut self, path: &Path) -> io::Result<()> { + let mut inner = self.inner.lock().unwrap(); + let mut path_buf = path.to_path_buf(); + while let Some(parent) = path_buf.parent() { + inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?; + path_buf.pop(); + } + inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir()) + } + fn remove_file(&mut self, path: &Path) -> io::Result<()> { let mut inner = self.inner.lock().unwrap(); diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index 7a348ff6..028a413b 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -71,6 +71,8 @@ pub trait VfsBackend: sealed::Sealed + Send + 'static { fn read(&mut self, path: &Path) -> io::Result>; fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>; fn read_dir(&mut self, path: &Path) -> io::Result; + fn create_dir(&mut self, path: &Path) -> io::Result<()>; + fn create_dir_all(&mut self, path: &Path) -> io::Result<()>; fn metadata(&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<()>; @@ -190,6 +192,16 @@ impl VfsInner { Ok(dir) } + fn create_dir>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.backend.create_dir(path) + } + + fn create_dir_all>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.backend.create_dir_all(path) + } + fn remove_file>(&mut self, path: P) -> io::Result<()> { let path = path.as_ref(); let _ = self.backend.unwatch(path); @@ -326,6 +338,31 @@ impl Vfs { self.inner.lock().unwrap().read_dir(path) } + /// Creates a directory at the provided location. + /// + /// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir]. + /// Similiar to that function, this function will fail if the parent of the + /// path does not exist. + /// + /// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html + #[inline] + pub fn create_dir>(&self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.lock().unwrap().create_dir(path) + } + + /// Creates a directory at the provided location, recursively creating + /// all parent components if they are missing. + /// + /// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all]. + /// + /// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html + #[inline] + pub fn create_dir_all>(&self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.lock().unwrap().create_dir_all(path) + } + /// Remove a file. /// /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. @@ -428,6 +465,31 @@ impl VfsLock<'_> { self.inner.read_dir(path) } + /// Creates a directory at the provided location. + /// + /// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir]. + /// Similiar to that function, this function will fail if the parent of the + /// path does not exist. + /// + /// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html + #[inline] + pub fn create_dir>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.create_dir(path) + } + + /// Creates a directory at the provided location, recursively creating + /// all parent components if they are missing. + /// + /// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all]. + /// + /// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html + #[inline] + pub fn create_dir_all>(&mut self, path: P) -> io::Result<()> { + let path = path.as_ref(); + self.inner.create_dir_all(path) + } + /// Remove a file. /// /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. diff --git a/crates/memofs/src/noop_backend.rs b/crates/memofs/src/noop_backend.rs index 31a2cca9..18f6f809 100644 --- a/crates/memofs/src/noop_backend.rs +++ b/crates/memofs/src/noop_backend.rs @@ -26,6 +26,20 @@ impl VfsBackend for NoopBackend { Err(io::Error::other("NoopBackend doesn't do anything")) } + fn create_dir(&mut self, _path: &Path) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::Other, + "NoopBackend doesn't do anything", + )) + } + + fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> { + Err(io::Error::new( + io::ErrorKind::Other, + "NoopBackend doesn't do anything", + )) + } + fn remove_file(&mut self, _path: &Path) -> io::Result<()> { Err(io::Error::other("NoopBackend doesn't do anything")) } diff --git a/crates/memofs/src/std_backend.rs b/crates/memofs/src/std_backend.rs index 54c142dc..d39fb179 100644 --- a/crates/memofs/src/std_backend.rs +++ b/crates/memofs/src/std_backend.rs @@ -78,6 +78,14 @@ impl VfsBackend for StdBackend { }) } + fn create_dir(&mut self, path: &Path) -> io::Result<()> { + fs_err::create_dir(path) + } + + fn create_dir_all(&mut self, path: &Path) -> io::Result<()> { + fs_err::create_dir_all(path) + } + fn remove_file(&mut self, path: &Path) -> io::Result<()> { fs_err::remove_file(path) } diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap new file mode 100644 index 00000000..7495dc1f --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__child_but_not-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing OnlyOneCopy/child_of_one.luau +Writing ReplicatedStorage/child_replicated_storage.luau diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap new file mode 100644 index 00000000..61fc2288 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__csv-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/csv.csv +Writing src/csv_init/init.csv +Writing src/csv_init diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap new file mode 100644 index 00000000..11b5f72c --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing container.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap new file mode 100644 index 00000000..3fc6b6b0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_adding-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/int_value.model.json +Writing src/subfolder/string_value.txt +Writing src/subfolder diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap new file mode 100644 index 00000000..6222321f --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_init-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/non-init.luau +Writing src/init-file diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap new file mode 100644 index 00000000..6f480488 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-src__Message.rbxm.snap @@ -0,0 +1,73 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: src/Message.rbxm +--- +num_types: 1 +num_instances: 1 +chunks: + - Inst: + type_id: 0 + type_name: Message + object_format: 0 + referents: + - 0 + - Prop: + type_id: 0 + prop_name: AttributesSerialize + prop_type: String + values: + - "" + - Prop: + type_id: 0 + prop_name: Capabilities + prop_type: SecurityCapabilities + values: + - 0 + - Prop: + type_id: 0 + prop_name: HistoryId + prop_type: UniqueId + values: + - "00000000000000000000000000000000" + - Prop: + type_id: 0 + prop_name: Name + prop_type: String + values: + - Message + - Prop: + type_id: 0 + prop_name: DefinesCapabilities + prop_type: Bool + values: + - false + - Prop: + type_id: 0 + prop_name: SourceAssetId + prop_type: Int64 + values: + - -1 + - Prop: + type_id: 0 + prop_name: Tags + prop_type: String + values: + - "" + - Prop: + type_id: 0 + prop_name: Text + prop_type: String + values: + - This message should be written to the disk. + - Prop: + type_id: 0 + prop_name: UniqueId + prop_type: UniqueId + values: + - 2030b7775c30713f085b9d4800005a8b + - Prnt: + version: 0 + links: + - - 0 + - -1 + - End diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap new file mode 100644 index 00000000..7449e6b1 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_paths_removing-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/Message.rbxm diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap new file mode 100644 index 00000000..f1f8c02c --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_adding-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/IncludeMe/.gitkeep +Writing src/IncludeMe diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap new file mode 100644 index 00000000..b6b9aaa0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ignore_trees_removing-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- + diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap new file mode 100644 index 00000000..f6e609d1 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__json_middlewares-stdout.snap @@ -0,0 +1,8 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/dir_with_meta/init.meta.json +Writing src/model_json.model.json +Writing src/project_json.project.json +Writing src/dir_with_meta diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap new file mode 100644 index 00000000..eab1bd54 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing nested.project.json +Writing string_value.txt diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap new file mode 100644 index 00000000..1f97ad7e --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/modules/ClientModule.luau +Writing src/modules/ServerModule.luau diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap new file mode 100644 index 00000000..409b4d57 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/init.luau +Writing src diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap new file mode 100644 index 00000000..d6e4238a --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing attribute_mismatch.luau +Writing property_mismatch.project.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap new file mode 100644 index 00000000..7fe594f1 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-src__ChildWithDuplicates.rbxm.snap @@ -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 diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap new file mode 100644 index 00000000..ecf33548 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback-stdout.snap @@ -0,0 +1,9 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/ChildWithDuplicates.rbxm +Writing src/ChildWithoutDuplicates/Child/.gitkeep +Writing src/ChildWithoutDuplicates +Writing src/ChildWithoutDuplicates/Child +Removing src/ChildWithDuplicates diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap new file mode 100644 index 00000000..774fe9d6 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/pointer.model.json +Writing src/target.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap new file mode 100644 index 00000000..a2cadfb2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_blank-stdout.snap @@ -0,0 +1,7 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/pointer.model.json +Writing src/target.meta.json +Writing src/target.txt diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap new file mode 100644 index 00000000..0942314d --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_conflict-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/Pointer_2.model.json +Writing src/Target_2.model.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap new file mode 100644 index 00000000..b6b9aaa0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_duplicate-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- + diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap new file mode 100644 index 00000000..0c748692 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-src__rbxm.rbxm.snap @@ -0,0 +1,55 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: src/rbxm.rbxm +--- +num_types: 1 +num_instances: 1 +chunks: + - Inst: + type_id: 0 + type_name: Folder + object_format: 0 + referents: + - 0 + - Prop: + type_id: 0 + prop_name: AttributesSerialize + prop_type: String + values: + - "" + - Prop: + type_id: 0 + prop_name: Capabilities + prop_type: SecurityCapabilities + values: + - 0 + - Prop: + type_id: 0 + prop_name: Name + prop_type: String + values: + - rbxm + - Prop: + type_id: 0 + prop_name: DefinesCapabilities + prop_type: Bool + values: + - false + - Prop: + type_id: 0 + prop_name: SourceAssetId + prop_type: Int64 + values: + - -1 + - Prop: + type_id: 0 + prop_name: Tags + prop_type: String + values: + - rbxmx + - Prnt: + version: 0 + links: + - - 0 + - -1 + - End diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap new file mode 100644 index 00000000..05dbd817 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__respect_old_middleware-stdout.snap @@ -0,0 +1,8 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json +Writing src/model_json.model.json +Writing src/rbxm.rbxm +Writing src/rbxmx.rbxmx diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap new file mode 100644 index 00000000..fc49a162 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json +Writing string_value.txt diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap new file mode 100644 index 00000000..00045825 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__sync_rules-stdout.snap @@ -0,0 +1,6 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing src/module.modulescript +Writing src/text.text diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap new file mode 100644 index 00000000..3ef76951 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__unscriptable_properties-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap new file mode 100644 index 00000000..b37618ce --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-OnlyOneCopy__child_of_one.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: OnlyOneCopy/child_of_one.luau +--- +-- this should be in OnlyOneCopy/child_of_one diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap new file mode 100644 index 00000000..8d7939df --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__child_but_not-ReplicatedStorage__child_replicated_storage.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: ReplicatedStorage/child_replicated_storage.luau +--- +-- -- this should be in child_replicated_storage diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap new file mode 100644 index 00000000..313f4c7e --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv.csv.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/csv.csv +--- +Key,Source,Context,Example,es +Ack,Ack!,,An exclamation of despair,¡Ay! diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap new file mode 100644 index 00000000..22266002 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__csv-src__csv_init__init.csv.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/csv_init/init.csv +--- +Key,Source,Context,Example,en +Rojo,Rojo,,Rojo is a really cool program,Red diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap new file mode 100644 index 00000000..b15329a2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__duplicate_rojo_id-container.model.json.snap @@ -0,0 +1,25 @@ +--- +source: tests/tests/syncback.rs +expression: container.model.json +--- +{ + "className": "Folder", + "children": [ + { + "name": "value_1", + "className": "ObjectValue", + "attributes": { + "Rojo_Id": "value_1", + "Rojo_Target_Value": "value_1" + } + }, + { + "name": "value_2", + "className": "ObjectValue", + "attributes": { + "Rojo_Id": "72bc28150ada2e6206442ee300004084", + "Rojo_Target_Value": "72bc28150ada2e6206442ee300004084" + } + } + ] +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap new file mode 100644 index 00000000..85e75bdc --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__int_value.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/int_value.model.json +--- +{ + "className": "IntValue", + "properties": { + "Value": 1337.0 + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap new file mode 100644 index 00000000..869aefb8 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_adding-src__subfolder__string_value.txt.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/subfolder/string_value.txt +--- +This memorial dedicated to Club Penguin. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap new file mode 100644 index 00000000..48375888 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__init-file__init.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/init-file/init.luau +--- +-- This file SHOULD NOT be updated diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap new file mode 100644 index 00000000..343aed6f --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ignore_paths_init-src__non-init.luau.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/non-init.luau +--- +-- This module SHOULD be updated +-- This text is new. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap new file mode 100644 index 00000000..8c56c92c --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__dir_with_meta__init.meta.json.snap @@ -0,0 +1,12 @@ +--- +source: tests/tests/syncback.rs +expression: src/dir_with_meta/init.meta.json +--- +{ + "properties": { + "Tags": [ + "This tag on dir_with_meta" + ] + }, + "className": "Configuration" +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap new file mode 100644 index 00000000..7e9aacf9 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__model_json.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/model_json.model.json +--- +{ + "className": "StringValue", + "properties": { + "Value": "This text is model_json" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap new file mode 100644 index 00000000..00bba2f3 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__json_middlewares-src__project_json.project.json.snap @@ -0,0 +1,17 @@ +--- +source: tests/tests/syncback.rs +expression: src/project_json.project.json +--- +{ + "name": "project_json", + "tree": { + "$className": "Color3Value", + "$properties": { + "Value": [ + 1337.0, + -1337.0, + 1337.0 + ] + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap new file mode 100644 index 00000000..5a2ebb8d --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-nested.project.json.snap @@ -0,0 +1,19 @@ +--- +source: tests/tests/syncback.rs +expression: nested.project.json +--- +{ + "name": "Nested", + "tree": { + "$className": "Configuration", + "BoolValue": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + }, + "StringValue": { + "$path": "string_value.txt" + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap new file mode 100644 index 00000000..7ed1790d --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects-string_value.txt.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: string_value.txt +--- +Nested project string value :-) diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap new file mode 100644 index 00000000..e070784a --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ClientModule.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/modules/ClientModule.luau +--- +-- Client module diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap new file mode 100644 index 00000000..cae56ba2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__nested_projects_weird-src__modules__ServerModule.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/modules/ServerModule.luau +--- +-- Server module diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap new file mode 100644 index 00000000..6c7428fb --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_init-src__init.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/init.luau +--- +-- Project init script diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap new file mode 100644 index 00000000..b59d4f60 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-attribute_mismatch.luau.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: attribute_mismatch.luau +--- +-- This script is a part of project_reserialize diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap new file mode 100644 index 00000000..a06e7f36 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_reserialize-property_mismatch.project.json.snap @@ -0,0 +1,15 @@ +--- +source: tests/tests/syncback.rs +expression: property_mismatch.project.json +--- +{ + "name": "property_mismatch", + "tree": { + "$className": "BrickColorValue", + "$properties": { + "Value": { + "BrickColor": 345 + } + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap new file mode 100644 index 00000000..3108d5c0 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__pointer.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/pointer.model.json +--- +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "test referent id" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap new file mode 100644 index 00000000..a3aa961b --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties-src__target.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/target.model.json +--- +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "test referent id" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap new file mode 100644 index 00000000..76186e98 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__pointer.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/pointer.model.json +--- +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "62e89c49e4f800c20629c71b00003fc0" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap new file mode 100644 index 00000000..83d77426 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.meta.json.snap @@ -0,0 +1,9 @@ +--- +source: tests/tests/syncback.rs +expression: src/target.meta.json +--- +{ + "attributes": { + "Rojo_Id": "62e89c49e4f800c20629c71b00003fc0" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap new file mode 100644 index 00000000..3d35ac47 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_blank-src__target.txt.snap @@ -0,0 +1,5 @@ +--- +source: tests/tests/syncback.rs +expression: src/target.txt +--- +This is a target. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap new file mode 100644 index 00000000..555c1851 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Pointer_2.model.json.snap @@ -0,0 +1,11 @@ +--- +source: tests/tests/syncback.rs +assertion_line: 28 +expression: src/Pointer_2.model.json +--- +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "0bd3e9e11879191708a2bc4a00000c20" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap new file mode 100644 index 00000000..2314c3d3 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__ref_properties_conflict-src__Target_2.model.json.snap @@ -0,0 +1,11 @@ +--- +source: tests/tests/syncback.rs +assertion_line: 28 +expression: src/Target_2.model.json +--- +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "0bd3e9e11879191708a2bc4a00000c20" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap new file mode 100644 index 00000000..315941cd --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-default.project.json.snap @@ -0,0 +1,16 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "respect_old_middleware", + "tree": { + "project_node": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + }, + "$path": "src" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap new file mode 100644 index 00000000..2c77ad07 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__model_json.model.json.snap @@ -0,0 +1,10 @@ +--- +source: tests/tests/syncback.rs +expression: src/model_json.model.json +--- +{ + "className": "StringValue", + "properties": { + "Value": "This should be a .model.json file" + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap new file mode 100644 index 00000000..14c517cc --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__respect_old_middleware-src__rbxmx.rbxmx.snap @@ -0,0 +1,16 @@ +--- +source: tests/tests/syncback.rs +expression: src/rbxmx.rbxmx +--- + + + + rbxmx + + 0 + false + -1 + cmJ4bXg= + + + diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap new file mode 100644 index 00000000..4e62a0c8 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__string_value_project-default.project.json.snap @@ -0,0 +1,22 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "string_value_project", + "tree": { + "$className": "Folder", + "inside_project_file": { + "$className": "StringValue", + "$properties": { + "Value": "imgettingverytiredofwritingthesetests2" + } + }, + "on_file_system": { + "$attributes": { + "imgettingverytiredofwritingthesetests": "person299 was ahead of his time" + }, + "$path": "string_value.txt" + } + } +} diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap new file mode 100644 index 00000000..7d01aac4 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__module.modulescript.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/module.modulescript +--- +-- This should be a in the file 'module.modulescript'. It should be updated to have a second line. +-- This is the second line in 'module.modulescript'. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap new file mode 100644 index 00000000..dc196fc9 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__sync_rules-src__text.text.snap @@ -0,0 +1,6 @@ +--- +source: tests/tests/syncback.rs +expression: src/text.text +--- +-- This should be a in the file 'text.text'. It should be updated to have a second line. +-- This is the second line in 'text.text'. diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap new file mode 100644 index 00000000..236064e3 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__unscriptable_properties-default.project.json.snap @@ -0,0 +1,18 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "unscriptable_properties", + "tree": { + "$className": "BinaryStringValue", + "$properties": { + "Value": { + "BinaryString": "Rojo/is/cool" + } + } + }, + "syncbackRules": { + "syncUnscriptable": true + } +} diff --git a/rojo-test/syncback-tests/child_but_not/input-project/OnlyOneCopy/.gitkeep b/rojo-test/syncback-tests/child_but_not/input-project/OnlyOneCopy/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/child_but_not/input-project/ReplicatedStorage/.gitkeep b/rojo-test/syncback-tests/child_but_not/input-project/ReplicatedStorage/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/child_but_not/input-project/default.project.json b/rojo-test/syncback-tests/child_but_not/input-project/default.project.json new file mode 100644 index 00000000..d23f524c --- /dev/null +++ b/rojo-test/syncback-tests/child_but_not/input-project/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "child_but_not", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "ReplicatedStorage", + "OnlyOneCopy": { + "$path": "OnlyOneCopy" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/child_but_not/input.rbxl b/rojo-test/syncback-tests/child_but_not/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..6a10886db3acbf943ca33c19dd6e908078fed37c GIT binary patch literal 56708 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 anO4Jp&R&`{e-OPXFFw=quQ&YLFaKX(lP%Z) literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/csv/input-project/default.project.json b/rojo-test/syncback-tests/csv/input-project/default.project.json new file mode 100644 index 00000000..da44ea35 --- /dev/null +++ b/rojo-test/syncback-tests/csv/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "csv", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv b/rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv new file mode 100644 index 00000000..24244d6c --- /dev/null +++ b/rojo-test/syncback-tests/csv/input-project/src/csv_init/init.csv @@ -0,0 +1,2 @@ +Key,Source,Context,Example,en +,,,, diff --git a/rojo-test/syncback-tests/csv/input.rbxm b/rojo-test/syncback-tests/csv/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..d42d3ef51925f57c84cae4848079db13b7dcf8b5 GIT binary patch literal 1041 zcmaJ>&2AGh5cVz*N<^vJQYwlHn^lXb0C7S{s6tw`K}rQ7N^(O`IU6@Jan{k=TP0L| z0Nw�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 RxGB<854VF$&0y{6{|i`LvG4!@ literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json b/rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json new file mode 100644 index 00000000..01e24a7a --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_adding/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "ignore_paths_adding", + "tree": { + "$path": "src" + }, + "syncbackRules": { + "ignorePaths": [ + "src/*.rbxm" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_paths_adding/input-project/src/.gitkeep b/rojo-test/syncback-tests/ignore_paths_adding/input-project/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ignore_paths_adding/input.rbxm b/rojo-test/syncback-tests/ignore_paths_adding/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..93331ba077c0b0e076b1f04ae73c0eed81dc01ca GIT binary patch literal 3670 zcmb7HTW=dh6y9}{G)+lJNG~+yvI!RXTuqhfx1)@r{`?+>X#lDF_& zp;nh+z?DO?KZWRQ6lPPl&&_Kj~k!T7&Zw>toChss~UoLV#9TdE5SnX3m;iR3(_pO ze4c>xfVo!S(K4=SIb8V93t!=ogkjlDF~1I*)i)L|k8=Nc+MH`!xDB zU?d5avzt9*1MXs?=tWZTiyJ%KSRYw)Pe0mlACz0Q)|gs`Mjz% zH^;%ldt)nI7gMq>#uT`?&3d}9eGj7~$rgs@ISqX;q`utl#>C&ao03vby^vb-|ZJ#h(CzDvXR#*mwU zAdD>6MscvFfV)Gz2p>p7>`1v@jr=~HWk75p4365P21&MnS>)Pm1Yri~RgD87Yb>hC zFcbl=*u5aoaKw;g6;KL$=aBhND2mvix^y+#^a9`^fYw}i9ovT_x1bTh292#)b(zoY zezCDE1AyGjB9EhCY$ovHu0WVWX^b4?j9mzwnwupGjKJZiTJr|)(g=)(B%8p(xC}OH zGFUZDJeYvYP^*(?`DW$n_3PhXymbD)+TZ^M{s3V>2uQLFG|nP_V&#+})XyL5i3olXT&n$;;`-a2KmYg(dW*JzmbF-eFa1j`d=@s89CC&1{GcSu!2Y>e zCR^74M_xFJSSRH5Fcv{wQfAeuZ^{Fsu4H6#^j(CUvbUV*R%f2P9$7Zca${SsL$lm1 z`Wb9I;oZEo3xHE&%eMuMd0UIzL}s{#%$kZQ1OSq_*c!s@MEXz0IkY?=PMOvr*E-F! z*-8g3xObc~W9vPw;aZBIevWJCKQ_O?0b|%Xqsu{LTIX})GfE~4&d^wsVQ?G5;V0*QrQzyqp^h6kRJUM-9}UV}$Hs;XZ$nrCfn^$Ti{?@5pA eoKz}H?tT2HSy^_E&K}tz&>eE{Rr+u8hyMW0<9;dt literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json b/rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json new file mode 100644 index 00000000..dabd1116 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_init/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "ignore_paths_init", + "tree": { + "$path": "src" + }, + "syncbackRules": { + "ignorePaths": [ + "**/init-file/*.luau" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau new file mode 100644 index 00000000..2879cb9b --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/init-file/init.luau @@ -0,0 +1 @@ +-- This file SHOULD NOT be updated diff --git a/rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau new file mode 100644 index 00000000..ddce6ce5 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_paths_init/input-project/src/non-init.luau @@ -0,0 +1 @@ +-- This module SHOULD be updated diff --git a/rojo-test/syncback-tests/ignore_paths_init/input.rbxm b/rojo-test/syncback-tests/ignore_paths_init/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..1cc07ef2376214a3200669039038f2994918cf76 GIT binary patch literal 1111 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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json b/rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json new file mode 100644 index 00000000..ec17e8f7 --- /dev/null +++ b/rojo-test/syncback-tests/ignore_trees_removing/input-project/default.project.json @@ -0,0 +1,14 @@ +{ + "name": "ignore_trees_removing", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + }, + "syncbackRules": { + "ignoreTrees": [ + "ReplicatedStorage/KeepMe" + ] + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ignore_trees_removing/input-project/src/KeepMe/.gitkeep b/rojo-test/syncback-tests/ignore_trees_removing/input-project/src/KeepMe/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/ignore_trees_removing/input.rbxl b/rojo-test/syncback-tests/ignore_trees_removing/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..422bbf3500ea00a47578b90fe54081c3784eb059 GIT binary patch literal 43090 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ρ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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/default.project.json b/rojo-test/syncback-tests/json_middlewares/input-project/default.project.json new file mode 100644 index 00000000..75b25839 --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "json_middleware", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json b/rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json new file mode 100644 index 00000000..d6202957 --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/src/dir_with_meta/init.meta.json @@ -0,0 +1,3 @@ +{ + "className": "Configuration" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json b/rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json new file mode 100644 index 00000000..a095016a --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/src/model_json.model.json @@ -0,0 +1,3 @@ +{ + "className": "StringValue" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json b/rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json new file mode 100644 index 00000000..ed845205 --- /dev/null +++ b/rojo-test/syncback-tests/json_middlewares/input-project/src/project_json.project.json @@ -0,0 +1,6 @@ +{ + "name": "project_json", + "tree": { + "$className": "Color3Value" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/json_middlewares/input.rbxm b/rojo-test/syncback-tests/json_middlewares/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..ef4d7d78032d244ccf22e05f503f754dd704df6c GIT binary patch literal 1551 zcmb7F-EPw`77yusd* zNAMfKcQTSKDnZJTj_ugz`#$HyZcntDCTi8Y{=K`?-C=Bl-H^H*Ux!iuh(?EhpWVJk zqYiPnS|)NPZ9lW>i;}4|h_yNsXW=+{PUELU!0n>FPaLR3>32VPk_m=9ge|B88Xy92 z53z!fflANiJk#7tl@@v*`U6^hOa$DvPwf`*lS*Qt3n5$I#yt{Y8)K_wIZ-~IJB7N@OL5sQ8g*1WX^FXD+TB)j4u)dj9 zh-hF{QJ~hUmz1wZ1Y8I5(yB-FfCDyJ$T&{K71zS7yM>VdyH!1GMTyE`JESB55pa9B ztj%0OL<3if0=2H-Viy<2wIMHEp6 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/nested_projects/expected/default.project.json b/rojo-test/syncback-tests/nested_projects/expected/default.project.json new file mode 100644 index 00000000..edb89e3c --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/expected/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "nested_projects", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Nested": { + "$path": "nested.project.json" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/expected/nested.project.json b/rojo-test/syncback-tests/nested_projects/expected/nested.project.json new file mode 100644 index 00000000..c71f3be9 --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/expected/nested.project.json @@ -0,0 +1,15 @@ +{ + "name": "Nested", + "tree": { + "$className": "Configuration", + "BoolValue": { + "$className": "BoolValue", + "$properties": { + "Value": true + } + }, + "StringValue": { + "$path": "string_value.txt" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/expected/string_value.txt b/rojo-test/syncback-tests/nested_projects/expected/string_value.txt new file mode 100644 index 00000000..cb6a6af2 --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/expected/string_value.txt @@ -0,0 +1 @@ +effective cover predict pawn south \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/input-project/default.project.json b/rojo-test/syncback-tests/nested_projects/input-project/default.project.json new file mode 100644 index 00000000..edb89e3c --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/input-project/default.project.json @@ -0,0 +1,11 @@ +{ + "name": "nested_projects", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Nested": { + "$path": "nested.project.json" + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/input-project/nested.project.json b/rojo-test/syncback-tests/nested_projects/input-project/nested.project.json new file mode 100644 index 00000000..8cc8ad1e --- /dev/null +++ b/rojo-test/syncback-tests/nested_projects/input-project/nested.project.json @@ -0,0 +1,12 @@ +{ + "name": "Nested", + "tree": { + "$className": "Configuration", + "StringValue": { + "$path": "string_value.txt" + }, + "BoolValue": { + "$className": "BoolValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/nested_projects/input-project/string_value.txt b/rojo-test/syncback-tests/nested_projects/input-project/string_value.txt new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/nested_projects/input.rbxl b/rojo-test/syncback-tests/nested_projects/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..4b47117252b47471fd350a58f8cb2d54fa407a35 GIT binary patch literal 56532 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

-Szi)%32^y&Dhwz}q{v-LtJ!I$J80 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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json b/rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json new file mode 100644 index 00000000..9c390105 --- /dev/null +++ b/rojo-test/syncback-tests/rbxm_fallback/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "rbxm_fallback", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/rbxm_fallback/input-project/src/ChildWithDuplicates/.gitkeep b/rojo-test/syncback-tests/rbxm_fallback/input-project/src/ChildWithDuplicates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/rbxm_fallback/input.rbxm b/rojo-test/syncback-tests/rbxm_fallback/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..37659d9445ca1600e77d08c17010072fe7a47255 GIT binary patch literal 549 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�^fKXmhih-kF1X?x+LH}PVV54~6Z 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 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json new file mode 100644 index 00000000..765340a2 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "ref_properties_conflict", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$path": "src" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json new file mode 100644 index 00000000..c39022dc --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_1.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json new file mode 100644 index 00000000..c39022dc --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Pointer_2.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json new file mode 100644 index 00000000..0e54f027 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_1.model.json @@ -0,0 +1,6 @@ +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json new file mode 100644 index 00000000..0e54f027 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_conflict/input-project/src/Target_2.model.json @@ -0,0 +1,6 @@ +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "identical ID" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_conflict/input.rbxl b/rojo-test/syncback-tests/ref_properties_conflict/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..0aee7958e9f5afb5f50fd8776a058860bd72be7f GIT binary patch literal 66581 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 MXa^UX!PRH~A0jwdC;$Ke literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json new file mode 100644 index 00000000..968375e9 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/default.project.json @@ -0,0 +1,6 @@ +{ + "name": "ref_properties_duplicate", + "tree": { + "$path": "src" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json new file mode 100644 index 00000000..3d078bc0 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_1.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "an ID that should not change" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json new file mode 100644 index 00000000..3d078bc0 --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Pointer_2.model.json @@ -0,0 +1,6 @@ +{ + "className": "ObjectValue", + "attributes": { + "Rojo_Target_Value": "an ID that should not change" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json new file mode 100644 index 00000000..8b47ff0c --- /dev/null +++ b/rojo-test/syncback-tests/ref_properties_duplicate/input-project/src/Target.model.json @@ -0,0 +1,6 @@ +{ + "className": "Folder", + "attributes": { + "Rojo_Id": "an ID that should not change" + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm b/rojo-test/syncback-tests/ref_properties_duplicate/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..f26cc34743b5ca47839e9d4c5d01c38c648ba967 GIT binary patch literal 990 zcmaiz+iuf95Qf*LrL-kNZPkK$Ko0b9Sq|c+sw$!;pdgW2iQ(4T#M5LelOy^f1-e%^$aomF*QFd?CaFTdmKXsI||*i`fge z;jy>Q*14XhYNTwdu=AlhSphYfO%@qEbkL8l3saud8_`AEj%0V0-L z!v?Et7sD$W*{?D!=uX9gANDW8FFYTyL9T)423yNoqlSg0d_YFYRDCCJSG<44Q_I5b zE%DgbS32${{x1763%c#VPNWU;i7wJ4$g~Yc6Pb+(o1m=&HproUfrFgOsT`_QSw(qs zQ#|lRh$(@JfXC+uSndg)Xsb<^Dx>^vA!A1VJDz=uG?oMAl6_BpBkvgphA!i&(Ni+m zEKUj#mzK~=B+ZyC7J13hAsggyMgbrLU6>KI@|tm; literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json b/rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json new file mode 100644 index 00000000..bac8234a --- /dev/null +++ b/rojo-test/syncback-tests/respect_old_middleware/input-project/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "respect_old_middleware", + "tree": { + "$path": "src", + "project_node": { + "$className": "BoolValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json new file mode 100644 index 00000000..a095016a --- /dev/null +++ b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/model_json.model.json @@ -0,0 +1,3 @@ +{ + "className": "StringValue" +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm b/rojo-test/syncback-tests/respect_old_middleware/input-project/src/rbxm.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..c29baaaf4a21a3fb63a4c23198136d5c63154794 GIT binary patch literal 446 zcmZWmZA!yH5Zx3JEmqMYqN29qe=ksk&_<|WEG8G&w4)59-LkuZ;0?T;N7)TN^QyeQ8lJ`)0A4|Z4hXzB&oxCGMd^<{>^10;fQ4BHo`-dnC3Pp%-#y5*? z{tAg89Ks$5$9kdc)FDXG_t9FU0 z1BtJNKsbW!2@BKMIi=2#pDr$;3KG;~XS_~)=s?D_Qop + true + null + nil + + + + 0 + false + rbxmx + -1 + + + + \ No newline at end of file diff --git a/rojo-test/syncback-tests/respect_old_middleware/input.rbxm b/rojo-test/syncback-tests/respect_old_middleware/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..6bee55304ff72a6fb8e8dc467c85711f33f6ffbc GIT binary patch literal 1254 zcmZ`(O>fgc5M7%zg_7`9REd-ix8wi^!T}+HRFDDUEJr@qzL65@&wVgpv-Q)kd_VzIw) z*liE9JFw1t7OSyJ+gYkVYBfzw1>Q8tL6_vLesn+{-vz0GkO z)=ohmWvMiMnW!*UKcw3YBe;zRBm(vZ#~SQ?I15KAR;iL^k4~bT&H{(>SWTt*KgwT2 zr)1r5B0ZLwCHhQ`)8SM{lGy|J_`!s|$*~UW>ns_|wlOk2iYOi7ke<3`vB|*8%H=1S^QmKT z==N4os8qT<;57nXEehCs3|NY{gGD0EjN+kdh7%Po7#dbsssf|iSiXGTv+4&eFMcqEr71TRq1ct!G+#(N3w3m1XD zYLY^^T;q8>kin@kfjQM#90em8gh9(~sl_b?$128!=nn3JSoGK%B=E#HAK2V+e+c1q Sx`$xozZyOdp67qJw|@bK<=+Yb literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/string_value_project/input-project/default.project.json b/rojo-test/syncback-tests/string_value_project/input-project/default.project.json new file mode 100644 index 00000000..13871dd2 --- /dev/null +++ b/rojo-test/syncback-tests/string_value_project/input-project/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "string_value_project", + "tree": { + "$className": "Folder", + "on_file_system": { + "$path": "string_value.txt" + }, + "inside_project_file": { + "$className": "StringValue" + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/string_value_project/input-project/string_value.txt b/rojo-test/syncback-tests/string_value_project/input-project/string_value.txt new file mode 100644 index 00000000..e69de29b diff --git a/rojo-test/syncback-tests/string_value_project/input.rbxm b/rojo-test/syncback-tests/string_value_project/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..7cafbe93fcf981363f9f54e587bbea5fe2bbef55 GIT binary patch literal 1033 zcmZ`(O>Yx15cMu4E&ZTsD}-nSbx;BIgwzA5QiT*~kcvbIO~f^FvXjJUcOBW@rs06N z@^6<*uP|@JCZMKIdb{hH=kfU2$*DEH*j&|q{@z;OUl$@2Lfm1N_}J*w4|(wPZy?_b z_-mKhxEja0uYEo9=7Z6xcPkwAHly=qyYq;@A2NY_4bKYmy@?~T0wn+lQCMQY$aj$0 zV79&0>F|q+Gb&m_mYWM(7oS@H&p>>SS@%4pmw9q+vRcLZ8x_l7#sTLaL&(VQD$8iK%bITkphflkT&J1Z>YH@6BSnku1Sv1ink9Dc$C@@+h~m~E5UKGmHg ze;!p4WdzV;)tzFE;f@-*`OYpmkRij!Hxa(Yi>hde&z!!;vS<9($&kEfk1xqi zy|xsY!NlrakstUGaWCY&3t`-ttTXBH$%&jOC)J2lB+WpMw3A*ZRBjAKf$LRtans*` z8vsiE!ici#r3M%JS3C~{epR8LJB;n>)ai)2V{5)r-**Q(CLFR!H^jK>rp{A>Rcjo3 z&IGR4P-Thzvo?dmf85^(P&4vMx$NA3gi-egzrSO^$oC<;%Pj70fc0*-y0WnP_$@2l z$Yfrwi2TSK$vN4xW|GQYHk2y$BNLn9RO(dDkK_d(NR)7-Z<0i%k!xKvJGe6CZGX)_ b=0F^V;r7h_pwT?zH15F+r!QxJ0B`;P$_Ud0 literal 0 HcmV?d00001 diff --git a/rojo-test/syncback-tests/sync_rules/input-project/default.project.json b/rojo-test/syncback-tests/sync_rules/input-project/default.project.json new file mode 100644 index 00000000..c6f08c56 --- /dev/null +++ b/rojo-test/syncback-tests/sync_rules/input-project/default.project.json @@ -0,0 +1,16 @@ +{ + "name": "sync_rules", + "tree": { + "$path": "src" + }, + "syncRules": [ + { + "use": "text", + "pattern": "*.text" + }, + { + "use": "moduleScript", + "pattern": "*.modulescript" + } + ] +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript b/rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript new file mode 100644 index 00000000..ef32f608 --- /dev/null +++ b/rojo-test/syncback-tests/sync_rules/input-project/src/module.modulescript @@ -0,0 +1 @@ +-- This should be a in the file 'module.modulescript'. It should be updated to have a second line. \ No newline at end of file diff --git a/rojo-test/syncback-tests/sync_rules/input-project/src/text.text b/rojo-test/syncback-tests/sync_rules/input-project/src/text.text new file mode 100644 index 00000000..0bbe8d4f --- /dev/null +++ b/rojo-test/syncback-tests/sync_rules/input-project/src/text.text @@ -0,0 +1 @@ +-- This should be a in the file 'text.text'. It should be updated to have a second line. \ No newline at end of file diff --git a/rojo-test/syncback-tests/sync_rules/input.rbxm b/rojo-test/syncback-tests/sync_rules/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..c6f3beef754eefb373b6a9dfb6b01ff6e6b382a6 GIT binary patch literal 1487 zcmb7ETW-@p6dk882!skMgm_O-ML;4UM5O_#fRfflK_o;ZBH{}id)f@RcH|kO7Q_#H zu>iy#*Z>=00W85{18~ozu1HHkxze4N&)m6l?(wWdVc!q8^Phg@vgfjlO|g+vXRK*= zYl}3xvO7-}EE=66x3`DBbfvDvI(!t$K&xq5)ln=OtxoqWjn9z7k|Stm$=5^Q6A?oQ z7=V|lzi4IzJb^FA$(x}U`=aAUa;Q^y2JlPt@j?pEWoUGayrUx-Y(8`RSfuFb_R}YA zWX;K8$w{;ew*fE|Cb|9TqXXx~$FSQgZk44zu;W%JTY{)>U zKX+ve92Y&0y<`ajGjP^&wuD98Gqn@AFCx0QLTpYexGpT2N1G$>gmL7Gno>eHyfl^t zJWe)uq&@*NCV1D`RC~7@=L5@=@qrx^@R1&HC?CilN`WQWH2Py1ycXW*qF6K3jSC9P zd(=T$TShRT;Q`I=k;9S_fS`6HrH8TftgAGAS6;2ImF-*0#d`hj?c!2(y;7`KD|WHG zytHoDD^#YC^L%eX-W+biG&tg?!|&R*F|oxAXVaXhF(;(+%(6b!qLb&Yu`rXr)93`yNX6M(`0aTpBb0~J#LRyEvshYz=*W!M$0 zNEnGn=`Ztx3xI38Tf-Tk8))O+R7`Xo455BB s7MZ--(O>22>Sg|Ma<1Nv<583%I2?iZ`ipYmhta@wAp!cQi{E|r1+k7+V*mgE literal 0 HcmV?d00001 diff --git a/src/change_processor.rs b/src/change_processor.rs index c5978b8c..78d45acc 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -183,7 +183,7 @@ impl JobThreadContext { if let Some(instigating_source) = &instance.metadata().instigating_source { match instigating_source { InstigatingSource::Path(path) => fs::remove_file(path).unwrap(), - InstigatingSource::ProjectNode(_, _, _, _) => { + InstigatingSource::ProjectNode { .. } => { log::warn!( "Cannot remove instance {:?}, it's from a project file", id @@ -231,7 +231,7 @@ impl JobThreadContext { log::warn!("Cannot change Source to non-string value."); } } - InstigatingSource::ProjectNode(_, _, _, _) => { + InstigatingSource::ProjectNode { .. } => { log::warn!( "Cannot remove instance {:?}, it's from a project file", id @@ -317,16 +317,21 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option< } }, - InstigatingSource::ProjectNode(project_path, instance_name, project_node, parent_class) => { + InstigatingSource::ProjectNode { + path, + name, + node, + parent_class, + } => { // This instance is the direct subject of a project node. Since // there might be information associated with our instance from // the project file, we snapshot the entire project node again. let snapshot_result = snapshot_project_node( &metadata.context, - project_path, - instance_name, - project_node, + path, + name, + node, vfs, parent_class.as_ref().map(|name| name.as_str()), ); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 065128fb..7d3d87b1 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,6 +7,7 @@ mod init; mod plugin; mod serve; mod sourcemap; +mod syncback; mod upload; use std::{borrow::Cow, env, path::Path, str::FromStr}; @@ -21,6 +22,7 @@ pub use self::init::{InitCommand, InitKind}; pub use self::plugin::{PluginCommand, PluginSubcommand}; pub use self::serve::ServeCommand; pub use self::sourcemap::SourcemapCommand; +pub use self::syncback::SyncbackCommand; pub use self::upload::UploadCommand; /// Command line options that Rojo accepts, defined using the clap crate. @@ -46,6 +48,7 @@ impl Options { Subcommand::FmtProject(subcommand) => subcommand.run(), Subcommand::Doc(subcommand) => subcommand.run(), Subcommand::Plugin(subcommand) => subcommand.run(), + Subcommand::Syncback(subcommand) => subcommand.run(self.global), } } } @@ -119,6 +122,7 @@ pub enum Subcommand { FmtProject(FmtProjectCommand), Doc(DocCommand), Plugin(PluginCommand), + Syncback(SyncbackCommand), } pub(super) fn resolve_path(path: &Path) -> Cow<'_, Path> { diff --git a/src/cli/syncback.rs b/src/cli/syncback.rs new file mode 100644 index 00000000..282d55c8 --- /dev/null +++ b/src/cli/syncback.rs @@ -0,0 +1,282 @@ +use std::{ + io::{self, BufReader, Write as _}, + mem::forget, + path::{Path, PathBuf}, + time::Instant, +}; + +use anyhow::Context; +use clap::Parser; +use fs_err::File; +use memofs::Vfs; +use rbx_dom_weak::{InstanceBuilder, WeakDom}; +use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor}; + +use crate::{ + path_serializer::display_absolute, + serve_session::ServeSession, + syncback::{syncback_loop, FsSnapshot}, +}; + +use super::{resolve_path, GlobalOptions}; + +const UNKNOWN_INPUT_KIND_ERR: &str = "Could not detect what kind of file was inputted. \ + Expected input file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx."; + +/// Performs 'syncback' for the provided project, using the `input` file +/// given. +/// +/// Syncback exists to convert Roblox files into a Rojo project automatically. +/// It uses the project.json file provided to traverse the Roblox file passed as +/// to serialize Instances to the file system in a format that Rojo understands. +/// +/// To ease programmatic use, this command pipes all normal output to stderr. +#[derive(Debug, Parser)] +pub struct SyncbackCommand { + /// Path to the project to sync back to. + #[clap(default_value = "")] + pub project: PathBuf, + + /// Path to the Roblox file to pull Instances from. + #[clap(long, short)] + pub input: PathBuf, + + /// If provided, a list all of the files and directories that will be + /// added or removed is emitted into stdout. + #[clap(long, short)] + pub list: bool, + + /// If provided, syncback will not actually write anything to the file + /// system. The command will otherwise run normally. + #[clap(long)] + pub dry_run: bool, + + /// If provided, the prompt for writing to the file system is skipped. + #[clap(long, short = 'y')] + pub non_interactive: bool, +} + +impl SyncbackCommand { + pub fn run(&self, global: GlobalOptions) -> anyhow::Result<()> { + let path_old = resolve_path(&self.project); + let path_new = resolve_path(&self.input); + + let input_kind = FileKind::from_path(&path_new).context(UNKNOWN_INPUT_KIND_ERR)?; + let dom_start_timer = Instant::now(); + let dom_new = read_dom(&path_new, input_kind)?; + log::debug!( + "Finished opening file in {:0.02}s", + dom_start_timer.elapsed().as_secs_f32() + ); + + let vfs = Vfs::new_default(); + vfs.set_watch_enabled(false); + + let project_start_timer = Instant::now(); + let session_old = ServeSession::new(vfs, path_old.clone())?; + log::debug!( + "Finished opening project in {:0.02}s", + project_start_timer.elapsed().as_secs_f32() + ); + + let mut dom_old = session_old.tree(); + + log::debug!("Old root: {}", dom_old.inner().root().class); + log::debug!("New root: {}", dom_new.root().class); + + if log::log_enabled!(log::Level::Trace) { + log::trace!("Children of old root:"); + for child in dom_old.inner().root().children() { + let inst = dom_old.get_instance(*child).unwrap(); + log::trace!("{} (class: {})", inst.name(), inst.class_name()); + } + log::trace!("Children of new root:"); + for child in dom_new.root().children() { + let inst = dom_new.get_by_ref(*child).unwrap(); + log::trace!("{} (class: {})", inst.name, inst.class); + } + } + + let syncback_timer = Instant::now(); + eprintln!("Beginning syncback..."); + let snapshot = syncback_loop( + session_old.vfs(), + &mut dom_old, + dom_new, + session_old.root_project(), + )?; + log::debug!( + "Syncback finished in {:.02}s!", + syncback_timer.elapsed().as_secs_f32() + ); + + let base_path = session_old.root_project().folder_location(); + if self.list { + list_files(&snapshot, global.color.into(), base_path)?; + } + + if !self.dry_run { + if !self.non_interactive { + eprintln!( + "Would write {} files/folders and remove {} files/folders.", + snapshot.added_paths().len(), + snapshot.removed_paths().len() + ); + eprint!("Is this okay? (Y/N): "); + io::stderr().flush()?; + let mut line = String::with_capacity(1); + io::stdin().read_line(&mut line)?; + line = line.trim().to_lowercase(); + if line != "y" { + eprintln!("Aborting due to user input!"); + return Ok(()); + } + } + eprintln!("Writing to the file system..."); + snapshot.write_to_vfs(base_path, session_old.vfs())?; + eprintln!("Finished syncback.") + } else { + eprintln!( + "Would write {} files/folders and remove {} files/folders.", + snapshot.added_paths().len(), + snapshot.removed_paths().len() + ); + eprintln!("Aborting before writing to file system due to `--dry-run`"); + } + + // It is potentially prohibitively expensive to drop a ServeSession, + // and the program is about to exit anyway so we're just going to forget + // about it. + drop(dom_old); + forget(session_old); + + Ok(()) + } +} + +fn read_dom(path: &Path, file_kind: FileKind) -> anyhow::Result { + let content = BufReader::new(File::open(path)?); + match file_kind { + FileKind::Rbxl => rbx_binary::from_reader(content).with_context(|| { + format!( + "Could not deserialize binary place file at {}", + path.display() + ) + }), + FileKind::Rbxlx => rbx_xml::from_reader(content, xml_decode_config()) + .with_context(|| format!("Could not deserialize XML place file at {}", path.display())), + FileKind::Rbxm => { + let temp_tree = rbx_binary::from_reader(content).with_context(|| { + format!( + "Could not deserialize binary place file at {}", + path.display() + ) + })?; + + process_model_dom(temp_tree) + } + FileKind::Rbxmx => { + let temp_tree = + rbx_xml::from_reader(content, xml_decode_config()).with_context(|| { + format!("Could not deserialize XML model file at {}", path.display()) + })?; + process_model_dom(temp_tree) + } + } +} + +fn process_model_dom(dom: WeakDom) -> anyhow::Result { + let temp_children = dom.root().children(); + if temp_children.len() == 1 { + let real_root = dom.get_by_ref(temp_children[0]).unwrap(); + let mut new_tree = WeakDom::new(InstanceBuilder::new(real_root.class)); + for (name, property) in &real_root.properties { + new_tree + .root_mut() + .properties + .insert(*name, property.to_owned()); + } + + let children = dom.clone_multiple_into_external(real_root.children(), &mut new_tree); + for child in children { + new_tree.transfer_within(child, new_tree.root_ref()); + } + Ok(new_tree) + } else { + anyhow::bail!( + "Rojo does not currently support models with more \ + than one Instance at the Root!" + ); + } +} + +fn xml_decode_config() -> rbx_xml::DecodeOptions<'static> { + rbx_xml::DecodeOptions::new().property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown) +} + +/// The different kinds of input that Rojo can syncback. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FileKind { + /// An XML model file. + Rbxmx, + + /// An XML place file. + Rbxlx, + + /// A binary model file. + Rbxm, + + /// A binary place file. + Rbxl, +} + +impl FileKind { + fn from_path(output: &Path) -> Option { + let extension = output.extension()?.to_str()?; + + match extension { + "rbxlx" => Some(FileKind::Rbxlx), + "rbxmx" => Some(FileKind::Rbxmx), + "rbxl" => Some(FileKind::Rbxl), + "rbxm" => Some(FileKind::Rbxm), + _ => None, + } + } +} + +fn list_files(snapshot: &FsSnapshot, color: ColorChoice, base_path: &Path) -> io::Result<()> { + let no_color = ColorSpec::new(); + let mut add_color = ColorSpec::new(); + add_color.set_fg(Some(Color::Green)); + let mut remove_color = ColorSpec::new(); + remove_color.set_fg(Some(Color::Red)); + + let writer = BufferWriter::stdout(color); + let mut buffer = writer.buffer(); + + let added = snapshot.added_paths(); + if !added.is_empty() { + buffer.set_color(&add_color)?; + for path in added { + writeln!( + &mut buffer, + "Writing {}", + display_absolute(path.strip_prefix(base_path).unwrap_or(path)) + )?; + } + } + let removed = snapshot.removed_paths(); + if !removed.is_empty() { + buffer.set_color(&remove_color)?; + for path in removed { + writeln!( + &mut buffer, + "Removing {}", + display_absolute(path.strip_prefix(base_path).unwrap_or(path)) + )?; + } + } + buffer.set_color(&no_color)?; + + writer.print(&buffer) +} diff --git a/src/lib.rs b/src/lib.rs index 864f8fdc..f1947eaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,9 +22,19 @@ mod serve_session; mod session_id; mod snapshot; mod snapshot_middleware; +mod syncback; +mod variant_eq; mod web; +// TODO: Work out what we should expose publicly + pub use project::*; pub use rojo_ref::*; pub use session_id::SessionId; +pub use snapshot::{ + InstanceContext, InstanceMetadata, InstanceSnapshot, InstanceWithMeta, InstanceWithMetaMut, + RojoDescendants, RojoTree, +}; +pub use snapshot_middleware::{snapshot_from_vfs, Middleware, ScriptType}; +pub use syncback::{syncback_loop, FsSnapshot, SyncbackData, SyncbackSnapshot}; pub use web::interface as web_api; diff --git a/src/path_serializer.rs b/src/path_serializer.rs index 29edaf82..2ddd3a4e 100644 --- a/src/path_serializer.rs +++ b/src/path_serializer.rs @@ -5,24 +5,32 @@ use std::path::Path; use serde::{ser::SerializeSeq, Serialize, Serializer}; -pub fn serialize_absolute(path: T, serializer: S) -> Result -where - S: Serializer, - T: AsRef, -{ +/// Converts the provided value into a String with all directory separators +/// converted into `/`. +pub fn display_absolute>(path: T) -> String { let as_str = path .as_ref() .as_os_str() .to_str() .expect("Invalid Unicode in file path, cannot serialize"); - let replaced = as_str.replace('\\', "/"); + as_str.replace('\\', "/") +} - serializer.serialize_str(&replaced) +/// A serializer for serde that serialize a value with all directory separators +/// converted into `/`. +pub fn serialize_absolute(path: T, serializer: S) -> Result +where + S: Serializer, + T: AsRef, +{ + serializer.serialize_str(&display_absolute(path)) } #[derive(Serialize)] struct WithAbsolute<'a>(#[serde(serialize_with = "serialize_absolute")] &'a Path); +/// A serializer for serde that serialize a list of values with all directory +/// separators converted into `/`. pub fn serialize_vec_absolute(paths: &[T], serializer: S) -> Result where S: Serializer, diff --git a/src/project.rs b/src/project.rs index a3bb00df..20c2dc9c 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, ffi::OsStr, fs, io, net::IpAddr, @@ -7,11 +7,13 @@ use std::{ }; use memofs::Vfs; -use rbx_dom_weak::{Ustr, UstrMap}; +use rbx_dom_weak::Ustr; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule}; +use crate::{ + glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule, syncback::SyncbackRules, +}; /// Represents 'default' project names that act as `init` files pub static DEFAULT_PROJECT_NAMES: [&str; 2] = ["default.project.json", "default.project.jsonc"]; @@ -114,6 +116,10 @@ pub struct Project { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub glob_ignore_paths: Vec, + /// A list of rules for syncback with this project file. + #[serde(skip_serializing_if = "Option::is_none")] + pub syncback_rules: Option, + /// A list of mappings of globs to syncing rules. If a file matches a glob, /// it will be 'transformed' into an Instance following the rule provided. /// Globs are relative to the folder the project file is in. @@ -332,12 +338,21 @@ pub enum PathNode { } impl PathNode { + /// Returns the path of the `PathNode`, without regard for if it's optional + // or not. + #[inline] pub fn path(&self) -> &Path { match self { PathNode::Required(pathbuf) => pathbuf, PathNode::Optional(OptionalPathNode { optional }) => optional, } } + + /// Returns whether this `PathNode` is optional or not. + #[inline] + pub fn is_optional(&self) -> bool { + matches!(self, PathNode::Optional(_)) + } } /// Describes an instance and its descendants in a project. @@ -367,16 +382,16 @@ pub struct ProjectNode { #[serde( rename = "$properties", default, - skip_serializing_if = "HashMap::is_empty" + skip_serializing_if = "BTreeMap::is_empty" )] - pub properties: UstrMap, + pub properties: BTreeMap, #[serde( rename = "$attributes", default, - skip_serializing_if = "HashMap::is_empty" + skip_serializing_if = "BTreeMap::is_empty" )] - pub attributes: HashMap, + pub attributes: BTreeMap, /// Defines the behavior when Rojo encounters unknown instances in Roblox /// Studio during live sync. `$ignoreUnknownInstances` should be considered diff --git a/src/resolution.rs b/src/resolution.rs index 86132c35..1e743efd 100644 --- a/src/resolution.rs +++ b/src/resolution.rs @@ -2,8 +2,8 @@ use std::borrow::Borrow; use anyhow::{bail, format_err}; use rbx_dom_weak::types::{ - Attributes, CFrame, Color3, Content, ContentId, Enum, Font, MaterialColors, Matrix3, Tags, - Variant, VariantType, Vector2, Vector3, + Attributes, CFrame, Color3, Content, ContentId, ContentType, Enum, Font, MaterialColors, + Matrix3, Tags, Variant, VariantType, Vector2, Vector3, }; use rbx_reflection::{DataType, PropertyDescriptor}; use serde::{Deserialize, Serialize}; @@ -37,6 +37,98 @@ impl UnresolvedValue { UnresolvedValue::Ambiguous(partial) => partial.resolve_unambiguous(), } } + + /// Creates an `UnresolvedValue` from a variant, using a class and property + /// name to potentially allow for ambiguous Enum variants. + pub fn from_variant(variant: Variant, class_name: &str, prop_name: &str) -> Self { + let descriptor = find_descriptor(class_name, prop_name); + if descriptor.is_some() { + // We can only use an ambiguous syntax if the property is known + // to the reflection database. + Self::Ambiguous(match variant { + Variant::Enum(rbx_enum) => { + if let Some(property) = descriptor { + if let DataType::Enum(enum_name) = &property.data_type { + let database = rbx_reflection_database::get().unwrap(); + if let Some(enum_descriptor) = database.enums.get(enum_name) { + for (variant_name, id) in &enum_descriptor.items { + if *id == rbx_enum.to_u32() { + return Self::Ambiguous(AmbiguousValue::String( + variant_name.to_string(), + )); + } + } + } + } + } + return Self::FullyQualified(variant); + } + Variant::Bool(bool) => AmbiguousValue::Bool(bool), + Variant::Float32(n) => AmbiguousValue::Number(n as f64), + Variant::Float64(n) => AmbiguousValue::Number(n), + Variant::Int32(n) => AmbiguousValue::Number(n as f64), + Variant::Int64(n) => AmbiguousValue::Number(n as f64), + Variant::String(str) => AmbiguousValue::String(str), + Variant::Tags(tags) => { + AmbiguousValue::StringArray(tags.iter().map(|s| s.to_string()).collect()) + } + Variant::Content(ref content) => match content.value() { + ContentType::None => AmbiguousValue::String(String::new()), + ContentType::Uri(uri) => AmbiguousValue::String(uri.clone()), + _ => return Self::FullyQualified(variant), + }, + Variant::ContentId(content) => AmbiguousValue::String(content.into_string()), + Variant::Vector2(vector) => { + AmbiguousValue::Array2([vector.x as f64, vector.y as f64]) + } + Variant::Vector3(vector) => { + AmbiguousValue::Array3([vector.x as f64, vector.y as f64, vector.z as f64]) + } + Variant::Color3(color) => { + AmbiguousValue::Array3([color.r as f64, color.g as f64, color.b as f64]) + } + Variant::CFrame(cf) => AmbiguousValue::Array12([ + cf.position.x as f64, + cf.position.y as f64, + cf.position.z as f64, + cf.orientation.x.x as f64, + cf.orientation.x.y as f64, + cf.orientation.x.z as f64, + cf.orientation.y.x as f64, + cf.orientation.y.y as f64, + cf.orientation.y.z as f64, + cf.orientation.z.x as f64, + cf.orientation.z.y as f64, + cf.orientation.z.z as f64, + ]), + Variant::Attributes(attr) => AmbiguousValue::Attributes(attr), + Variant::Font(font) => AmbiguousValue::Font(font), + Variant::MaterialColors(colors) => AmbiguousValue::MaterialColors(colors), + _ => { + return Self::FullyQualified(variant); + } + }) + } else { + Self::FullyQualified(variant) + } + } + + /// Creates an `UnresolvedValue` from a variant, only returning ambiguous + /// values if they're able to be resolved in a context-free environment. + pub fn from_variant_unambiguous(variant: Variant) -> Self { + match variant { + Variant::String(str) => Self::Ambiguous(AmbiguousValue::String(str)), + Variant::Float64(number) => Self::Ambiguous(AmbiguousValue::Number(number)), + Variant::Bool(bool) => Self::Ambiguous(AmbiguousValue::Bool(bool)), + Variant::BinaryString(bstr) => match std::str::from_utf8(bstr.as_ref()) { + Ok(_) => Self::Ambiguous(AmbiguousValue::String( + String::from_utf8(bstr.into_vec()).unwrap(), + )), + Err(_) => Self::FullyQualified(Variant::BinaryString(bstr)), + }, + _ => Self::FullyQualified(variant), + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/serve_session.rs b/src/serve_session.rs index cb3e6f5b..f50d3926 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -210,6 +210,10 @@ impl ServeSession { pub fn root_dir(&self) -> &Path { self.root_project.folder_location() } + + pub fn root_project(&self) -> &Project { + &self.root_project + } } #[derive(Debug, Error)] diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs index 20505f29..71a731ef 100644 --- a/src/snapshot/metadata.rs +++ b/src/snapshot/metadata.rs @@ -62,6 +62,10 @@ pub struct InstanceMetadata { /// Indicates the ID used for Ref properties pointing to this Instance. pub specified_id: Option, + + /// The Middleware that was used to create this Instance. Should generally + /// not be `None` except if the snapshotting process is not completed. + pub middleware: Option, } impl InstanceMetadata { @@ -72,6 +76,7 @@ impl InstanceMetadata { relevant_paths: Vec::new(), context: InstanceContext::default(), specified_id: None, + middleware: None, } } @@ -109,6 +114,13 @@ impl InstanceMetadata { ..self } } + + pub fn middleware(self, middleware: Middleware) -> Self { + Self { + middleware: Some(middleware), + ..self + } + } } impl Default for InstanceMetadata { @@ -215,22 +227,40 @@ impl PathIgnoreRule { } } +/// Represents where a particular Instance or InstanceSnapshot came from. #[derive(Clone, PartialEq, Serialize, Deserialize)] pub enum InstigatingSource { + /// The path the Instance was made from. Path(#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf), - ProjectNode( - #[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf, - String, - Box, - Option, - ), + /// The node in a Project that the Instance was made from. + ProjectNode { + #[serde(serialize_with = "path_serializer::serialize_absolute")] + path: PathBuf, + name: String, + node: ProjectNode, + parent_class: Option, + }, +} + +impl InstigatingSource { + pub fn path(&self) -> &Path { + match self { + Self::Path(path) => path.as_path(), + Self::ProjectNode { path, .. } => path.as_path(), + } + } } impl fmt::Debug for InstigatingSource { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()), - InstigatingSource::ProjectNode(path, name, node, parent_class) => write!( + InstigatingSource::ProjectNode { + name, + node, + path, + parent_class, + } => write!( formatter, "ProjectNode({}: {:?}) from path {} and parent class {:?}", name, diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap index 2f3852b5..c4390b6d 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap @@ -14,5 +14,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap index 31bdfecc..fa4d3ebc 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap @@ -12,5 +12,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap index cafc3f5e..f07da090 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap @@ -14,5 +14,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap index 359ee86e..cc582633 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap @@ -12,5 +12,6 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ children: [] diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap index 9147ca9b..4c404604 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap @@ -13,6 +13,7 @@ added_instances: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: New class_name: Folder properties: {} diff --git a/src/snapshot/tree.rs b/src/snapshot/tree.rs index 76bcdc07..9e891f2c 100644 --- a/src/snapshot/tree.rs +++ b/src/snapshot/tree.rs @@ -73,6 +73,13 @@ impl RojoTree { self.inner.root_ref() } + /// Returns the root Instance of this tree. + #[inline] + pub fn root(&self) -> InstanceWithMeta<'_> { + self.get_instance(self.get_root_id()) + .expect("RojoTrees should have a root") + } + pub fn get_instance(&self, id: Ref) -> Option> { if let Some(instance) = self.inner.get_by_ref(id) { let metadata = self.metadata_map.get(&id).unwrap(); @@ -322,6 +329,10 @@ impl<'a> InstanceWithMeta<'a> { pub fn metadata(&self) -> &'a InstanceMetadata { self.metadata } + + pub fn inner(&self) -> &Instance { + self.instance + } } /// RojoTree's equivalent of `&'a mut Instance`. @@ -371,6 +382,14 @@ impl InstanceWithMetaMut<'_> { pub fn metadata(&self) -> &InstanceMetadata { self.metadata } + + pub fn inner(&self) -> &Instance { + self.instance + } + + pub fn inner_mut(&mut self) -> &mut Instance { + self.instance + } } #[cfg(test)] diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs index e81aa1c3..75a043d1 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -1,16 +1,24 @@ -use std::{collections::BTreeMap, path::Path}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + path::Path, +}; use anyhow::Context; use memofs::Vfs; -use rbx_dom_weak::ustr; -use serde::Serialize; +use rbx_dom_weak::{types::Variant, ustr}; +use serde::{Deserialize, Serialize}; use crate::{ snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, - snapshot_middleware::meta_file::DirectoryMetadata, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, }; -use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata}; +use super::{ + dir::{snapshot_dir_no_meta, syncback_dir_no_meta}, + meta_file::{AdjacentMetadata, DirectoryMetadata}, + PathExt as _, +}; pub fn snapshot_csv( _context: &InstanceContext, @@ -51,9 +59,10 @@ pub fn snapshot_csv_init( context: &InstanceContext, vfs: &Vfs, init_path: &Path, + name: &str, ) -> anyhow::Result> { let folder_path = init_path.parent().unwrap(); - let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap(); + let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path, name)?.unwrap(); if dir_snapshot.class_name != "Folder" { anyhow::bail!( @@ -70,33 +79,111 @@ pub fn snapshot_csv_init( init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; + // The directory snapshot middleware includes all possible init paths + // so we don't need to add it here. DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?; Ok(Some(init_snapshot)) } +pub fn syncback_csv<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = + if let Some(Variant::String(content)) = new_inst.properties.get(&ustr("Contents")) { + content.as_str() + } else { + anyhow::bail!("LocalizationTables must have a `Contents` property that is a String") + }; + let mut fs_snapshot = FsSnapshot::new(); + fs_snapshot.add_file(&snapshot.path, localization_to_csv(contents)?); + + let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(mut meta) = meta { + // LocalizationTables have relatively few properties that we care + // about, so shifting is fine. + meta.properties.shift_remove(&ustr("Contents")); + + if !meta.is_empty() { + let parent = snapshot.path.parent_err()?; + fs_snapshot.add_file( + parent.join(format!("{}.meta.json", new_inst.name)), + serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?, + ) + } + } + + Ok(SyncbackReturn { + fs_snapshot, + children: Vec::new(), + removed_children: Vec::new(), + }) +} + +pub fn syncback_csv_init<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = + if let Some(Variant::String(content)) = new_inst.properties.get(&ustr("Contents")) { + content.as_str() + } else { + anyhow::bail!("LocalizationTables must have a `Contents` property that is a String") + }; + + let mut dir_syncback = syncback_dir_no_meta(snapshot)?; + dir_syncback.fs_snapshot.add_file( + snapshot.path.join("init.csv"), + localization_to_csv(contents)?, + ); + + let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?; + if let Some(mut meta) = meta { + // LocalizationTables have relatively few properties that we care + // about, so shifting is fine. + meta.properties.shift_remove(&ustr("Contents")); + 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")?, + ); + } + } + + Ok(dir_syncback) +} + /// Struct that holds any valid row from a Roblox CSV translation table. /// /// We manually deserialize into this table from CSV, but let serde_json handle /// serialization. -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct LocalizationEntry<'a> { #[serde(skip_serializing_if = "Option::is_none")] - key: Option<&'a str>, + key: Option>, #[serde(skip_serializing_if = "Option::is_none")] - context: Option<&'a str>, + context: Option>, + + // Roblox writes `examples` for LocalizationTable's Content property, which + // causes it to not roundtrip correctly. + // This is reported here: https://devforum.roblox.com/t/2908720. + // + // To support their mistake, we support an alias named `examples`. + #[serde(skip_serializing_if = "Option::is_none", alias = "examples")] + example: Option>, #[serde(skip_serializing_if = "Option::is_none")] - example: Option<&'a str>, - - #[serde(skip_serializing_if = "Option::is_none")] - source: Option<&'a str>, + source: Option>, // We use a BTreeMap here to get deterministic output order. - values: BTreeMap<&'a str, &'a str>, + values: BTreeMap, Cow<'a, str>>, } /// Normally, we'd be able to let the csv crate construct our struct for us. @@ -130,12 +217,14 @@ fn convert_localization_csv(contents: &[u8]) -> Result { } match header { - "Key" => entry.key = Some(value), - "Source" => entry.source = Some(value), - "Context" => entry.context = Some(value), - "Example" => entry.example = Some(value), + "Key" => entry.key = Some(Cow::Borrowed(value)), + "Source" => entry.source = Some(Cow::Borrowed(value)), + "Context" => entry.context = Some(Cow::Borrowed(value)), + "Example" => entry.example = Some(Cow::Borrowed(value)), _ => { - entry.values.insert(header, value); + entry + .values + .insert(Cow::Borrowed(header), Cow::Borrowed(value)); } } } @@ -153,6 +242,57 @@ fn convert_localization_csv(contents: &[u8]) -> Result { Ok(encoded) } +/// Takes a localization table (as a string) and converts it into a CSV file. +/// +/// The CSV file is ordered, so it should be deterministic. +fn localization_to_csv(csv_contents: &str) -> anyhow::Result> { + let mut out = Vec::new(); + let mut writer = csv::Writer::from_writer(&mut out); + + let mut csv: Vec = + serde_json::from_str(csv_contents).context("cannot decode JSON from localization table")?; + + // TODO sort this better + csv.sort_by(|a, b| a.source.partial_cmp(&b.source).unwrap()); + + let mut headers = vec!["Key", "Source", "Context", "Example"]; + // We want both order and a lack of duplicates, so we use a BTreeSet. + let mut extra_headers = BTreeSet::new(); + for entry in &csv { + for lang in entry.values.keys() { + extra_headers.insert(lang.as_ref()); + } + } + headers.extend(extra_headers.iter()); + + writer + .write_record(&headers) + .context("could not write headers for localization table")?; + + let mut record: Vec<&str> = Vec::with_capacity(headers.len()); + for entry in &csv { + record.push(entry.key.as_deref().unwrap_or_default()); + record.push(entry.source.as_deref().unwrap_or_default()); + record.push(entry.context.as_deref().unwrap_or_default()); + record.push(entry.example.as_deref().unwrap_or_default()); + + let values = &entry.values; + for header in &extra_headers { + record.push(values.get(*header).map(AsRef::as_ref).unwrap_or_default()); + } + + writer + .write_record(&record) + .context("cannot write record for localization table")?; + record.clear(); + } + + // We must drop `writer` here to regain access to `out`. + drop(writer); + + Ok(out) +} + #[cfg(test)] mod test { use super::*; @@ -240,6 +380,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.csv"), + "root", ) .unwrap() .unwrap(); @@ -277,6 +418,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.csv"), + "root", ) .unwrap() .unwrap(); diff --git a/src/snapshot_middleware/dir.rs b/src/snapshot_middleware/dir.rs index 18959cd5..b825542d 100644 --- a/src/snapshot_middleware/dir.rs +++ b/src/snapshot_middleware/dir.rs @@ -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> { - 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> { 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> { + 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> { + 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::>().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); } diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs index 2b472095..a6f0148f 100644 --- a/src/snapshot_middleware/json_model.rs +++ b/src/snapshot_middleware/json_model.rs @@ -1,17 +1,19 @@ -use std::{borrow::Cow, collections::HashMap, path::Path, str}; +use std::{borrow::Cow, path::Path, str}; use anyhow::Context; +use indexmap::IndexMap; use memofs::Vfs; use rbx_dom_weak::{ - types::{Attributes, Ref}, + types::{Attributes, Ref, Variant}, HashMapExt as _, Ustr, UstrMap, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::{ json, resolution::UnresolvedValue, snapshot::{InstanceContext, InstanceSnapshot}, + syncback::{filter_properties_preallocated, FsSnapshot, SyncbackReturn, SyncbackSnapshot}, RojoRef, }; @@ -63,13 +65,86 @@ pub fn snapshot_json_model( Ok(Some(snapshot)) } -#[derive(Debug, Deserialize)] +pub fn syncback_json_model<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let mut property_buffer = Vec::with_capacity(snapshot.new_inst().properties.len()); + + let mut model = json_model_from_pair(snapshot, &mut property_buffer, snapshot.new); + // We don't need the name on the root, but we do for children. + model.name = None; + + Ok(SyncbackReturn { + fs_snapshot: FsSnapshot::new().with_added_file( + &snapshot.path, + serde_json::to_vec_pretty(&model).context("failed to serialize new JSON Model")?, + ), + children: Vec::new(), + removed_children: Vec::new(), + }) +} + +fn json_model_from_pair<'sync>( + snapshot: &SyncbackSnapshot<'sync>, + prop_buffer: &mut Vec<(Ustr, &'sync Variant)>, + new: Ref, +) -> JsonModel { + let new_inst = snapshot + .get_new_instance(new) + .expect("all new referents passed to json_model_from_pair should exist"); + + filter_properties_preallocated(snapshot.project(), new_inst, prop_buffer); + + let mut properties = IndexMap::new(); + let mut attributes = IndexMap::new(); + for (name, value) in prop_buffer.drain(..) { + match value { + Variant::Attributes(attrs) => { + for (attr_name, attr_value) in attrs.iter() { + // We (probably) don't want to preserve internal attributes, + // only user defined ones. + if attr_name.starts_with("RBX") { + continue; + } + attributes.insert( + attr_name.clone(), + UnresolvedValue::from_variant_unambiguous(attr_value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), &new_inst.class, &name), + ); + } + } + } + + let mut children = Vec::with_capacity(new_inst.children().len()); + + for new_child_ref in new_inst.children() { + children.push(json_model_from_pair(snapshot, prop_buffer, *new_child_ref)) + } + + JsonModel { + name: Some(new_inst.name.clone()), + class_name: new_inst.class, + children, + properties, + attributes, + id: None, + schema: None, + } +} + +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct JsonModel { #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] schema: Option, - #[serde(alias = "Name")] + #[serde(alias = "Name", skip_serializing_if = "Option::is_none")] name: Option, #[serde(alias = "ClassName")] @@ -87,13 +162,13 @@ struct JsonModel { #[serde( alias = "Properties", - default = "UstrMap::new", - skip_serializing_if = "HashMap::is_empty" + default, + skip_serializing_if = "IndexMap::is_empty" )] - properties: UstrMap, + properties: IndexMap, - #[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")] - attributes: HashMap, + #[serde(default = "IndexMap::new", skip_serializing_if = "IndexMap::is_empty")] + attributes: IndexMap, } impl JsonModel { diff --git a/src/snapshot_middleware/lua.rs b/src/snapshot_middleware/lua.rs index 32638d7f..eaa90952 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -1,11 +1,22 @@ use std::{path::Path, str}; +use anyhow::Context as _; use memofs::Vfs; -use rbx_dom_weak::{types::Enum, ustr, HashMapExt as _, UstrMap}; +use rbx_dom_weak::{ + types::{Enum, Variant}, + ustr, HashMapExt as _, UstrMap, +}; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; -use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata, meta_file::DirectoryMetadata}; +use super::{ + dir::{snapshot_dir_no_meta, syncback_dir_no_meta}, + meta_file::{AdjacentMetadata, DirectoryMetadata}, + PathExt as _, +}; #[derive(Debug)] pub enum ScriptType { @@ -95,10 +106,11 @@ pub fn snapshot_lua_init( context: &InstanceContext, vfs: &Vfs, init_path: &Path, + name: &str, script_type: ScriptType, ) -> anyhow::Result> { let folder_path = init_path.parent().unwrap(); - let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap(); + let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path, name)?.unwrap(); if dir_snapshot.class_name != "Folder" { anyhow::bail!( @@ -117,12 +129,89 @@ pub fn snapshot_lua_init( init_snapshot.children = dir_snapshot.children; init_snapshot.metadata = dir_snapshot.metadata; + // The directory snapshot middleware includes all possible init paths + // so we don't need to add it here. DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?; Ok(Some(init_snapshot)) } +pub fn syncback_lua<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + + let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) { + source.as_bytes().to_vec() + } else { + anyhow::bail!("Scripts must have a `Source` 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 { + // Scripts have relatively few properties that we care about, so shifting + // is fine. + meta.properties.shift_remove(&ustr("Source")); + + if !meta.is_empty() { + let parent_location = snapshot.path.parent_err()?; + fs_snapshot.add_file( + parent_location.join(format!("{}.meta.json", new_inst.name)), + serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?, + ); + } + } + + Ok(SyncbackReturn { + fs_snapshot, + // Scripts don't have a child! + children: Vec::new(), + removed_children: Vec::new(), + }) +} + +pub fn syncback_lua_init<'sync>( + script_type: ScriptType, + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let new_inst = snapshot.new_inst(); + let path = snapshot.path.join(match script_type { + ScriptType::Server => "init.server.luau", + ScriptType::Client => "init.client.luau", + ScriptType::Module => "init.luau", + _ => anyhow::bail!("syncback is not yet implemented for {script_type:?}"), + }); + + let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) { + source.as_bytes().to_vec() + } else { + anyhow::bail!("Scripts must have a `Source` property that is a String") + }; + + let mut dir_syncback = syncback_dir_no_meta(snapshot)?; + dir_syncback.fs_snapshot.add_file(&path, contents); + + let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, path.clone())?; + if let Some(mut meta) = meta { + // Scripts have relatively few properties that we care about, so shifting + // is fine. + meta.properties.shift_remove(&ustr("Source")); + + 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")?, + ); + } + } + + Ok(dir_syncback) +} + #[cfg(test)] mod test { use super::*; @@ -305,6 +394,7 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.lua"), + "root", ScriptType::Module, ) .unwrap() @@ -336,6 +426,7 @@ mod test { &InstanceContext::with_emit_legacy_scripts(Some(true)), &vfs, Path::new("/root/init.lua"), + "root", ScriptType::Module, ) .unwrap() diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs index 59acc8d5..1757dee1 100644 --- a/src/snapshot_middleware/meta_file.rs +++ b/src/snapshot_middleware/meta_file.rs @@ -1,14 +1,18 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; use anyhow::{format_err, Context}; +use indexmap::IndexMap; use memofs::{IoResultExt as _, Vfs}; -use rbx_dom_weak::{types::Attributes, Ustr, UstrMap}; +use rbx_dom_weak::{ + types::{Attributes, Variant}, + Ustr, +}; use serde::{Deserialize, Serialize}; -use crate::{json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, RojoRef}; +use crate::{ + json, resolution::UnresolvedValue, snapshot::InstanceSnapshot, syncback::SyncbackSnapshot, + RojoRef, +}; /// Represents metadata in a sibling file with the same basename. /// @@ -26,11 +30,11 @@ pub struct AdjacentMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub ignore_unknown_instances: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub properties: UstrMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub properties: IndexMap, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub attributes: HashMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub attributes: IndexMap, #[serde(skip)] pub path: PathBuf, @@ -80,6 +84,76 @@ impl AdjacentMetadata { Ok(meta) } + /// Constructs an `AdjacentMetadata` from the provided snapshot, assuming it + /// will be at the provided path. + pub fn from_syncback_snapshot( + snapshot: &SyncbackSnapshot, + path: PathBuf, + ) -> anyhow::Result> { + let mut properties = IndexMap::new(); + let mut attributes = IndexMap::new(); + // TODO make this more granular. + // I am breaking the cycle of bad TODOs. This is in reference to the fact + // that right now, this will just not write any metadata at all for + // project nodes, which is not always desirable. We should try to be + // smarter about it. + if let Some(old_inst) = snapshot.old_inst() { + if let Some(source) = &old_inst.metadata().instigating_source { + let source = source.path(); + if source != path { + log::debug!( + "Instigating source for Instance is mismatched so its metadata is being skipped.\nPath: {}", + path.display() + ); + return Ok(None); + } + } + } + + let ignore_unknown_instances = snapshot + .old_inst() + .map(|inst| inst.metadata().ignore_unknown_instances) + .unwrap_or_default(); + + let class = &snapshot.new_inst().class; + for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() { + match value { + Variant::Attributes(attrs) => { + for (attr_name, attr_value) in attrs.iter() { + // We (probably) don't want to preserve internal + // attributes, only user defined ones. + if attr_name.starts_with("RBX") { + continue; + } + attributes.insert( + attr_name.clone(), + UnresolvedValue::from_variant_unambiguous(attr_value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), class, &name), + ); + } + } + } + + Ok(Some(Self { + ignore_unknown_instances: if ignore_unknown_instances { + Some(true) + } else { + None + }, + properties, + attributes, + path, + id: None, + schema: None, + })) + } + pub fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) { if let Some(ignore) = self.ignore_unknown_instances.take() { snapshot.metadata.ignore_unknown_instances = ignore; @@ -89,7 +163,10 @@ impl AdjacentMetadata { pub fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { let path = &self.path; - for (key, unresolved) in self.properties.drain() { + // BTreeMaps don't have an equivalent to HashMap::drain, so the next + // best option is to take ownership of the entire map. Not free, but + // very cheap. + for (key, unresolved) in std::mem::take(&mut self.properties) { let value = unresolved .resolve(&snapshot.class_name, &key) .with_context(|| format!("error applying meta file {}", path.display()))?; @@ -100,7 +177,7 @@ impl AdjacentMetadata { if !self.attributes.is_empty() { let mut attributes = Attributes::new(); - for (key, unresolved) in self.attributes.drain() { + for (key, unresolved) in std::mem::take(&mut self.attributes) { let value = unresolved.resolve_unambiguous()?; attributes.insert(key, value); } @@ -131,6 +208,18 @@ impl AdjacentMetadata { Ok(()) } + /// Returns whether the metadata is 'empty', meaning it doesn't have anything + /// worth persisting in it. Specifically: + /// + /// - The number of properties and attributes is 0 + /// - `ignore_unknown_instances` is None + #[inline] + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + && self.properties.is_empty() + && self.ignore_unknown_instances.is_none() + } + // TODO: Add method to allow selectively applying parts of metadata and // throwing errors if invalid parts are specified. } @@ -151,11 +240,11 @@ pub struct DirectoryMetadata { #[serde(skip_serializing_if = "Option::is_none")] pub ignore_unknown_instances: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub properties: UstrMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub properties: IndexMap, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub attributes: HashMap, + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub attributes: IndexMap, #[serde(skip_serializing_if = "Option::is_none")] pub class_name: Option, @@ -207,6 +296,80 @@ impl DirectoryMetadata { Ok(meta) } + /// Constructs a `DirectoryMetadata` from the provided snapshot, assuming it + /// will be at the provided path. + /// + /// This function does not set `ClassName` manually as most uses won't + /// want it set. + pub fn from_syncback_snapshot( + snapshot: &SyncbackSnapshot, + path: PathBuf, + ) -> anyhow::Result> { + let mut properties = IndexMap::new(); + let mut attributes = IndexMap::new(); + // TODO make this more granular. + // I am breaking the cycle of bad TODOs. This is in reference to the fact + // that right now, this will just not write any metadata at all for + // project nodes, which is not always desirable. We should try to be + // smarter about it. + if let Some(old_inst) = snapshot.old_inst() { + if let Some(source) = &old_inst.metadata().instigating_source { + let source = source.path(); + if source != path { + log::debug!( + "Instigating source for Instance is mismatched so its metadata is being skipped.\nPath: {}", + path.display() + ); + return Ok(None); + } + } + } + + let ignore_unknown_instances = snapshot + .old_inst() + .map(|inst| inst.metadata().ignore_unknown_instances) + .unwrap_or_default(); + + let class = &snapshot.new_inst().class; + for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() { + match value { + Variant::Attributes(attrs) => { + for (name, value) in attrs.iter() { + // We (probably) don't want to preserve internal + // attributes, only user defined ones. + if name.starts_with("RBX") { + continue; + } + attributes.insert( + name.to_owned(), + UnresolvedValue::from_variant_unambiguous(value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), class, &name), + ); + } + } + } + + Ok(Some(Self { + ignore_unknown_instances: if ignore_unknown_instances { + Some(true) + } else { + None + }, + properties, + attributes, + class_name: None, + path, + id: None, + schema: None, + })) + } + pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { self.apply_ignore_unknown_instances(snapshot); self.apply_class_name(snapshot)?; @@ -241,7 +404,7 @@ impl DirectoryMetadata { fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> { let path = &self.path; - for (key, unresolved) in self.properties.drain() { + for (key, unresolved) in std::mem::take(&mut self.properties) { let value = unresolved .resolve(&snapshot.class_name, &key) .with_context(|| format!("error applying meta file {}", path.display()))?; @@ -252,7 +415,7 @@ impl DirectoryMetadata { if !self.attributes.is_empty() { let mut attributes = Attributes::new(); - for (key, unresolved) in self.attributes.drain() { + for (key, unresolved) in std::mem::take(&mut self.attributes) { let value = unresolved.resolve_unambiguous()?; attributes.insert(key, value); } @@ -275,6 +438,53 @@ impl DirectoryMetadata { snapshot.metadata.specified_id = self.id.take().map(RojoRef::new); Ok(()) } + + /// Returns whether the metadata is 'empty', meaning it doesn't have anything + /// worth persisting in it. Specifically: + /// + /// - The number of properties and attributes is 0 + /// - `ignore_unknown_instances` is None + /// - `class_name` is either None or not Some("Folder") + #[inline] + pub fn is_empty(&self) -> bool { + self.attributes.is_empty() + && self.properties.is_empty() + && self.ignore_unknown_instances.is_none() + && if let Some(class) = &self.class_name { + class == "Folder" + } else { + true + } + } +} + +/// Retrieves the meta file that should be applied for the provided directory, +/// if it exists. +pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result> { + let meta_path = path.join("init.meta.json"); + + if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? { + let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?; + Ok(Some(metadata)) + } else { + Ok(None) + } +} + +/// Retrieves the meta file that should be applied for the provided file, +/// if it exists. +/// +/// The `name` field should be the name the metadata should have. +pub fn file_meta(vfs: &Vfs, path: &Path, name: &str) -> anyhow::Result> { + let mut meta_path = path.with_file_name(name); + meta_path.set_extension("meta.json"); + + if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? { + let metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?; + Ok(Some(metadata)) + } else { + Ok(None) + } } #[cfg(test)] diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs index 30c5d13b..9eec5f83 100644 --- a/src/snapshot_middleware/mod.rs +++ b/src/snapshot_middleware/mod.rs @@ -28,24 +28,34 @@ use anyhow::Context; use memofs::{IoResultExt, Vfs}; use serde::{Deserialize, Serialize}; -use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule}; -use crate::{glob::Glob, project::DEFAULT_PROJECT_NAMES}; +use crate::{ + glob::Glob, + project::DEFAULT_PROJECT_NAMES, + syncback::{SyncbackReturn, SyncbackSnapshot}, +}; +use crate::{ + snapshot::{InstanceContext, InstanceSnapshot, SyncRule}, + syncback::validate_file_name, +}; use self::{ - csv::{snapshot_csv, snapshot_csv_init}, - dir::snapshot_dir, + csv::{snapshot_csv, snapshot_csv_init, syncback_csv, syncback_csv_init}, + dir::{snapshot_dir, syncback_dir}, json::snapshot_json, - json_model::snapshot_json_model, - lua::{snapshot_lua, snapshot_lua_init, ScriptType}, - project::snapshot_project, - rbxm::snapshot_rbxm, - rbxmx::snapshot_rbxmx, + json_model::{snapshot_json_model, syncback_json_model}, + lua::{snapshot_lua, snapshot_lua_init, syncback_lua, syncback_lua_init}, + project::{snapshot_project, syncback_project}, + rbxm::{snapshot_rbxm, syncback_rbxm}, + rbxmx::{snapshot_rbxmx, syncback_rbxmx}, toml::snapshot_toml, - txt::snapshot_txt, + txt::{snapshot_txt, syncback_txt}, yaml::snapshot_yaml, }; -pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default}; +pub use self::{ + lua::ScriptType, project::snapshot_project_node, util::emit_legacy_scripts_default, + util::PathExt, +}; /// Returns an `InstanceSnapshot` for the provided path. /// This will inspect the path and find the appropriate middleware for it, @@ -63,41 +73,14 @@ pub fn snapshot_from_vfs( }; if meta.is_dir() { - if let Some(init_path) = get_init_path(vfs, path)? { - // TODO: support user-defined init paths - // If and when we do, make sure to go support it in - // `Project::set_file_name`, as right now it special-cases - // `default.project.json` as an `init` path. - for rule in default_sync_rules() { - if rule.matches(&init_path) { - return match rule.middleware { - Middleware::Project => { - let name = init_path - .parent() - .and_then(Path::file_name) - .and_then(|s| s.to_str()).expect("default.project.json should be inside a folder with a unicode name"); - snapshot_project(context, vfs, &init_path, name) - } - - Middleware::ModuleScript => { - snapshot_lua_init(context, vfs, &init_path, ScriptType::Module) - } - Middleware::ServerScript => { - snapshot_lua_init(context, vfs, &init_path, ScriptType::Server) - } - Middleware::ClientScript => { - snapshot_lua_init(context, vfs, &init_path, ScriptType::Client) - } - - Middleware::Csv => snapshot_csv_init(context, vfs, &init_path), - - _ => snapshot_dir(context, vfs, path), - }; - } - } - snapshot_dir(context, vfs, path) - } else { - snapshot_dir(context, vfs, path) + let (middleware, dir_name, init_path) = get_dir_middleware(vfs, path)?; + // TODO: Support user defined init paths + // If and when we do, make sure to go support it in + // `Project::set_file_name`, as right now it special-cases + // `default.project.json` as an `init` path. + match middleware { + Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name), + _ => middleware.snapshot(context, vfs, &init_path, dir_name), } } else { let file_name = path @@ -116,55 +99,50 @@ pub fn snapshot_from_vfs( } } -/// Gets an `init` path for the given directory. -/// This uses an intrinsic priority list and for compatibility, -/// it should not be changed. -fn get_init_path>(vfs: &Vfs, dir: P) -> anyhow::Result> { - let path = dir.as_ref(); +/// Gets the appropriate middleware for a directory by checking for `init` +/// files. This uses an intrinsic priority list and for compatibility, +/// that order should be left unchanged. +/// +/// Returns the middleware, the name of the directory, and the path to +/// the init location. +fn get_dir_middleware<'path>( + vfs: &Vfs, + dir_path: &'path Path, +) -> anyhow::Result<(Middleware, &'path str, PathBuf)> { + let dir_name = dir_path + .file_name() + .expect("Could not extract directory name") + .to_str() + .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?; + + static INIT_PATHS: OnceLock> = OnceLock::new(); + let order = INIT_PATHS.get_or_init(|| { + vec![ + (Middleware::ModuleScriptDir, "init.luau"), + (Middleware::ModuleScriptDir, "init.lua"), + (Middleware::ServerScriptDir, "init.server.luau"), + (Middleware::ServerScriptDir, "init.server.lua"), + (Middleware::ClientScriptDir, "init.client.luau"), + (Middleware::ClientScriptDir, "init.client.lua"), + (Middleware::CsvDir, "init.csv"), + ] + }); for default_project_name in DEFAULT_PROJECT_NAMES { - let project_path = path.join(default_project_name); + let project_path = dir_path.join(default_project_name); if vfs.metadata(&project_path).with_not_found()?.is_some() { - return Ok(Some(project_path)); + return Ok((Middleware::Project, dir_name, project_path)); } } - let init_path = path.join("init.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); + for (middleware, name) in order { + let test_path = dir_path.join(name); + if vfs.metadata(&test_path).with_not_found()?.is_some() { + return Ok((*middleware, dir_name, test_path)); + } } - let init_path = path.join("init.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.server.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.server.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.client.luau"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.client.lua"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - let init_path = path.join("init.csv"); - if vfs.metadata(&init_path).with_not_found()?.is_some() { - return Ok(Some(init_path)); - } - - Ok(None) + Ok((Middleware::Dir, dir_name, dir_path.to_path_buf())) } /// Gets a snapshot for a path given an InstanceContext and Vfs, taking @@ -194,9 +172,10 @@ fn snapshot_from_path( } /// Represents a possible 'transformer' used by Rojo to turn a file system -/// item into a Roblox Instance. Missing from this list are directories and -/// metadata. This is deliberate, as metadata is not a snapshot middleware -/// and directories do not make sense to turn into files. +/// item into a Roblox Instance. Missing from this list is metadata. +/// This is deliberate, as metadata is not a snapshot middleware. +/// +/// Directories cannot be used for sync rules so they're ignored by Serde. #[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum Middleware { @@ -218,6 +197,17 @@ pub enum Middleware { Text, Yaml, Ignore, + + #[serde(skip_deserializing)] + Dir, + #[serde(skip_deserializing)] + ServerScriptDir, + #[serde(skip_deserializing)] + ClientScriptDir, + #[serde(skip_deserializing)] + ModuleScriptDir, + #[serde(skip_deserializing)] + CsvDir, } impl Middleware { @@ -230,7 +220,7 @@ impl Middleware { path: &Path, name: &str, ) -> anyhow::Result> { - match self { + let mut output = match self { Self::Csv => snapshot_csv(context, vfs, path, name), Self::JsonModel => snapshot_json_model(context, vfs, path, name), Self::Json => snapshot_json(context, vfs, path, name), @@ -257,6 +247,120 @@ impl Middleware { Self::Text => snapshot_txt(context, vfs, path, name), Self::Yaml => snapshot_yaml(context, vfs, path, name), Self::Ignore => Ok(None), + + Self::Dir => snapshot_dir(context, vfs, path, name), + Self::ServerScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Server) + } + Self::ClientScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Client) + } + Self::ModuleScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Module) + } + Self::CsvDir => snapshot_csv_init(context, vfs, path, name), + }; + if let Ok(Some(ref mut snapshot)) = output { + snapshot.metadata.middleware = Some(*self); + } + output + } + + /// Runs the syncback mechanism for the provided middleware given a + /// SyncbackSnapshot. + pub fn syncback<'sync>( + &self, + snapshot: &SyncbackSnapshot<'sync>, + ) -> anyhow::Result> { + let file_name = snapshot.path.file_name().and_then(|s| s.to_str()); + if let Some(file_name) = file_name { + validate_file_name(file_name).with_context(|| { + format!("cannot create a file or directory with name {file_name}") + })?; + } + match self { + Middleware::Csv => syncback_csv(snapshot), + Middleware::JsonModel => syncback_json_model(snapshot), + Middleware::Json => anyhow::bail!("cannot syncback Json middleware"), + // Projects are only generated from files that already exist on the + // file system, so we don't need to pass a file name. + Middleware::Project => syncback_project(snapshot), + Middleware::ServerScript => syncback_lua(snapshot), + Middleware::ClientScript => syncback_lua(snapshot), + Middleware::ModuleScript => syncback_lua(snapshot), + Middleware::Rbxm => syncback_rbxm(snapshot), + Middleware::Rbxmx => syncback_rbxmx(snapshot), + Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"), + Middleware::Text => syncback_txt(snapshot), + Middleware::Yaml => anyhow::bail!("cannot syncback Yaml middleware"), + Middleware::Ignore => anyhow::bail!("cannot syncback Ignore middleware"), + Middleware::Dir => syncback_dir(snapshot), + Middleware::ServerScriptDir => syncback_lua_init(ScriptType::Server, snapshot), + Middleware::ClientScriptDir => syncback_lua_init(ScriptType::Client, snapshot), + Middleware::ModuleScriptDir => syncback_lua_init(ScriptType::Module, snapshot), + Middleware::CsvDir => syncback_csv_init(snapshot), + + Middleware::PluginScript + | Middleware::LegacyServerScript + | Middleware::LegacyClientScript + | Middleware::RunContextServerScript + | Middleware::RunContextClientScript => { + anyhow::bail!("syncback is not implemented for {self:?} yet") + } + } + } + + /// Returns whether this particular middleware would become a directory. + #[inline] + pub fn is_dir(&self) -> bool { + matches!( + self, + Middleware::Dir + | Middleware::ServerScriptDir + | Middleware::ClientScriptDir + | Middleware::ModuleScriptDir + | Middleware::CsvDir + ) + } + + /// Returns whether this particular middleware sets its own properties. + /// This applies to things like `JsonModel` and `Project`, since they + /// set properties without needing a meta.json file. + /// + /// It does not cover middleware like `ServerScript` or `Csv` because they + /// need a meta.json file to set properties that aren't their designated + /// 'special' properties. + #[inline] + pub fn handles_own_properties(&self) -> bool { + matches!( + self, + Middleware::JsonModel | Middleware::Project | Middleware::Rbxm | Middleware::Rbxmx + ) + } + + /// Attempts to return a middleware that should be used for the given path. + /// + /// Returns `Err` only if the Vfs cannot read information about the path. + pub fn middleware_for_path( + vfs: &Vfs, + sync_rules: &[SyncRule], + path: &Path, + ) -> anyhow::Result> { + let meta = match vfs.metadata(path).with_not_found()? { + Some(meta) => meta, + None => return Ok(None), + }; + + if meta.is_dir() { + let (middleware, _, _) = get_dir_middleware(vfs, path)?; + Ok(Some(middleware)) + } else { + for rule in sync_rules.iter().chain(default_sync_rules()) { + if rule.matches(path) { + return Ok(Some(rule.middleware)); + } + } + Ok(None) } } } diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs index 7d951f86..c114ac5d 100644 --- a/src/snapshot_middleware/project.rs +++ b/src/snapshot_middleware/project.rs @@ -1,19 +1,27 @@ -use std::{borrow::Cow, path::Path}; +use std::{ + borrow::Cow, + collections::{BTreeMap, HashMap, VecDeque}, + path::Path, +}; use anyhow::{bail, Context}; use memofs::Vfs; use rbx_dom_weak::{ - types::{Attributes, Ref}, - ustr, HashMapExt as _, Ustr, UstrMap, + types::{Attributes, Ref, Variant}, + ustr, HashMapExt as _, Instance, Ustr, UstrMap, }; use rbx_reflection::ClassTag; use crate::{ project::{PathNode, Project, ProjectNode}, + resolution::UnresolvedValue, snapshot::{ - InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule, - SyncRule, + InstanceContext, InstanceMetadata, InstanceSnapshot, InstanceWithMeta, InstigatingSource, + PathIgnoreRule, SyncRule, }, + snapshot_middleware::Middleware, + syncback::{filter_properties, FsSnapshot, SyncbackReturn, SyncbackSnapshot}, + variant_eq::variant_eq, RojoRef, }; @@ -286,12 +294,12 @@ pub fn snapshot_project_node( metadata.specified_id = Some(RojoRef::new(id.clone())) } - metadata.instigating_source = Some(InstigatingSource::ProjectNode( - project_path.to_path_buf(), - instance_name.to_string(), - Box::new(node.clone()), - parent_class.map(|name| name.to_owned()), - )); + metadata.instigating_source = Some(InstigatingSource::ProjectNode { + path: project_path.to_path_buf(), + name: instance_name.to_string(), + node: node.clone(), + parent_class: parent_class.map(|name| name.to_owned()), + }); Ok(Some(InstanceSnapshot { snapshot_id: Ref::none(), @@ -303,6 +311,318 @@ pub fn snapshot_project_node( })) } +pub fn syncback_project<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let old_inst = snapshot + .old_inst() + .expect("projects should always exist in both trees"); + // Generally, the path of a project is the first thing added to the relevant + // paths. So, we take the last one. + let project_path = old_inst + .metadata() + .relevant_paths + .last() + .expect("all projects should have a relevant path"); + let vfs = snapshot.vfs(); + + log::debug!("Reloading project {} from vfs", project_path.display(),); + let mut project = Project::load_exact(vfs, project_path, None)?; + let base_path = project.folder_location().to_path_buf(); + + // Sync rules for this project do not have their base rule set but it is + // important when performing syncback on other projects. + for rule in &mut project.sync_rules { + rule.base_path.clone_from(&base_path) + } + + let mut descendant_snapshots = Vec::new(); + let mut removed_descendants = Vec::new(); + + let mut ref_to_path_map = HashMap::new(); + let mut old_child_map = HashMap::new(); + let mut new_child_map = HashMap::new(); + + let mut node_changed_map = Vec::new(); + let mut node_queue = VecDeque::with_capacity(1); + node_queue.push_back((&mut project.tree, old_inst, snapshot.new_inst())); + + while let Some((node, old_inst, new_inst)) = node_queue.pop_front() { + log::debug!("Processing node {}", old_inst.name()); + if old_inst.class_name() != new_inst.class { + anyhow::bail!( + "Cannot change the class of {} in project file {}.\n\ + Current class is {}, it is a {} in the input file.", + old_inst.name(), + project_path.display(), + old_inst.class_name(), + new_inst.class + ); + } + + // TODO handle meta.json files in this branch. Right now, we perform + // syncback if a node has `$path` set but the Middleware aren't aware + // that the Instances they're running on originate in a project.json. + // As a result, the `meta.json` syncback code is hardcoded to not work + // if the Instance originates from a project file. However, we should + // ideally use a .meta.json over the project node if it exists already. + if node.path.is_some() { + // Since the node has a path, we have to run syncback on it. + let node_path = node.path.as_ref().map(PathNode::path).expect( + "Project nodes with a path must have a path \ + If you see this message, something went seriously wrong. Please report it.", + ); + let full_path = if node_path.is_absolute() { + node_path.to_path_buf() + } else { + base_path.join(node_path) + }; + + let middleware = match Middleware::middleware_for_path( + snapshot.vfs(), + &project.sync_rules, + &full_path, + )? { + Some(middleware) => middleware, + // The only way this can happen at this point is if the path does + // not exist on the file system or there's no middleware for it. + None => anyhow::bail!( + "path does not exist or could not be turned into a file Rojo understands: {}", + full_path.display() + ), + }; + + descendant_snapshots.push( + snapshot + .with_new_path(full_path.clone(), new_inst.referent(), Some(old_inst.id())) + .middleware(middleware), + ); + + ref_to_path_map.insert(new_inst.referent(), full_path); + + // We only want to set properties if it needs it. + if !middleware.handles_own_properties() { + project_node_property_syncback_path(snapshot, new_inst, node); + } + } else { + project_node_property_syncback_no_path(snapshot, new_inst, node); + } + + for child_ref in new_inst.children() { + let child = snapshot + .get_new_instance(*child_ref) + .expect("all children of Instances should be in new DOM"); + if new_child_map.insert(&child.name, child).is_some() { + anyhow::bail!( + "Instances that are direct children of an Instance that is made by a project file \ + must have a unique name.\nThe child '{}' of '{}' is duplicated in the place file.", child.name, old_inst.name() + ); + } + } + for child_ref in old_inst.children() { + let child = snapshot + .get_old_instance(*child_ref) + .expect("all children of Instances should be in old DOM"); + if old_child_map.insert(child.name(), child).is_some() { + anyhow::bail!( + "Instances that are direct children of an Instance that is made by a project file \ + must have a unique name.\nThe child '{}' of '{}' is duplicated on the file system.", child.name(), old_inst.name() + ); + } + } + + // This loop does basic matching of Instance children to the node's + // children. It ensures that `new_child_map` and `old_child_map` will + // only contain Instances that don't belong to the project after this. + for (child_name, child_node) in &mut node.children { + // If a node's path is optional, we want to skip it if the path + // doesn't exist since it isn't in the current old DOM. + if let Some(path) = &child_node.path { + if path.is_optional() { + let real_path = if path.path().is_absolute() { + path.path().to_path_buf() + } else { + base_path.join(path.path()) + }; + if !real_path.exists() { + log::warn!( + "Skipping node '{child_name}' of project because it is optional and not present on the disk.\n\ + If this is not deliberate, please create a file or directory at {}", real_path.display() + ); + continue; + } + } + } + let new_equivalent = new_child_map.remove(child_name); + let old_equivalent = old_child_map.remove(child_name.as_str()); + match (new_equivalent, old_equivalent) { + (Some(new), Some(old)) => node_queue.push_back((child_node, old, new)), + (_, None) => anyhow::bail!( + "The child '{child_name}' of Instance '{}' would be removed.\n\ + Syncback cannot add or remove Instances from project {}", + old_inst.name(), + project_path.display() + ), + (None, _) => anyhow::bail!( + "The child '{child_name}' of Instance '{}' is present only in a project file,\n\ + and not the provided file. Syncback cannot add or remove Instances from project:\n{}.", + old_inst.name(), project_path.display(), + ) + } + } + + // All of the children in this loop are by their nature not in the + // project, so we just need to run syncback on them. + for (name, new_child) in new_child_map.drain() { + let parent_path = match ref_to_path_map.get(&new_child.parent()) { + Some(path) => path.clone(), + None => { + log::debug!("Skipping child {name} of node because it has no parent_path"); + continue; + } + }; + + // If a child also exists in the old tree, it will be caught in the + // syncback on the project node path above (or is itself a node). + // So the only things we need to run seperately is new children. + if old_child_map.remove(name.as_str()).is_none() { + let parent_middleware = + Middleware::middleware_for_path(vfs, &project.sync_rules, &parent_path)? + .expect("project nodes should have a middleware if they have children."); + // If this node points directly to a project, it may still have + // children but they'll be handled by syncback. This isn't a + // concern with directories because they're singular things, + // files that contain their own children. + if parent_middleware != Middleware::Project { + descendant_snapshots.push(snapshot.with_base_path( + &parent_path, + new_child.referent(), + None, + )?); + } + } + } + removed_descendants.extend(old_child_map.drain().map(|(_, v)| v)); + node_changed_map.push((&node.properties, &node.attributes, old_inst)) + } + let mut fs_snapshot = FsSnapshot::new(); + + for (node_properties, node_attributes, old_inst) in node_changed_map { + if project_node_should_reserialize(node_properties, node_attributes, old_inst)? { + fs_snapshot.add_file(project_path, serde_json::to_vec_pretty(&project)?); + break; + } + } + + Ok(SyncbackReturn { + fs_snapshot, + children: descendant_snapshots, + removed_children: removed_descendants, + }) +} + +fn project_node_property_syncback( + _snapshot: &SyncbackSnapshot, + filtered_properties: UstrMap<&Variant>, + new_inst: &Instance, + node: &mut ProjectNode, +) { + let properties = &mut node.properties; + let mut attributes = BTreeMap::new(); + for (name, value) in filtered_properties { + match value { + Variant::Attributes(attrs) => { + for (attr_name, attr_value) in attrs.iter() { + // We (probably) don't want to preserve internal attributes, + // only user defined ones. + if attr_name.starts_with("RBX") { + continue; + } + attributes.insert( + attr_name.clone(), + UnresolvedValue::from_variant_unambiguous(attr_value.clone()), + ); + } + } + _ => { + properties.insert( + name, + UnresolvedValue::from_variant(value.clone(), &new_inst.class, &name), + ); + } + } + } + node.attributes = attributes; +} + +fn project_node_property_syncback_path( + snapshot: &SyncbackSnapshot, + new_inst: &Instance, + node: &mut ProjectNode, +) { + let filtered_properties = snapshot + .get_path_filtered_properties(new_inst.referent()) + .unwrap(); + project_node_property_syncback(snapshot, filtered_properties, new_inst, node) +} + +fn project_node_property_syncback_no_path( + snapshot: &SyncbackSnapshot, + new_inst: &Instance, + node: &mut ProjectNode, +) { + let filtered_properties = filter_properties(snapshot.project(), new_inst); + project_node_property_syncback(snapshot, filtered_properties, new_inst, node) +} + +fn project_node_should_reserialize( + node_properties: &BTreeMap, + node_attributes: &BTreeMap, + instance: InstanceWithMeta, +) -> anyhow::Result { + for (prop_name, unresolved_node_value) in node_properties { + if let Some(inst_value) = instance.properties().get(prop_name) { + let node_value = unresolved_node_value + .clone() + .resolve(&instance.class_name(), prop_name)?; + if !variant_eq(inst_value, &node_value) { + return Ok(true); + } + } else { + return Ok(true); + } + } + + match instance.properties().get(&ustr("Attributes")) { + Some(Variant::Attributes(inst_attributes)) => { + // This will also catch if one is empty but the other isn't + if node_attributes.len() != inst_attributes.len() { + Ok(true) + } else { + for (attr_name, unresolved_node_value) in node_attributes { + if let Some(inst_value) = inst_attributes.get(attr_name.as_str()) { + let node_value = unresolved_node_value.clone().resolve_unambiguous()?; + if !variant_eq(inst_value, &node_value) { + return Ok(true); + } + } else { + return Ok(true); + } + } + Ok(false) + } + } + Some(_) => Ok(true), + None => { + if !node_attributes.is_empty() { + Ok(true) + } else { + Ok(false) + } + } + } +} + fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option { // If className wasn't defined from another source, we may be able // to infer one. diff --git a/src/snapshot_middleware/rbxm.rs b/src/snapshot_middleware/rbxm.rs index f2c08dd0..f3693d52 100644 --- a/src/snapshot_middleware/rbxm.rs +++ b/src/snapshot_middleware/rbxm.rs @@ -3,7 +3,10 @@ use std::path::Path; use anyhow::Context; use memofs::Vfs; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; #[profiling::function] pub fn snapshot_rbxm( @@ -39,6 +42,24 @@ pub fn snapshot_rbxm( } } +pub fn syncback_rbxm<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let inst = snapshot.new_inst(); + + // Long-term, we probably want to have some logic for if this contains a + // script. That's a future endeavor though. + let mut serialized = Vec::new(); + rbx_binary::to_writer(&mut serialized, snapshot.new_tree(), &[inst.referent()]) + .context("failed to serialize new rbxm")?; + + Ok(SyncbackReturn { + fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), + children: Vec::new(), + removed_children: Vec::new(), + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/snapshot_middleware/rbxmx.rs b/src/snapshot_middleware/rbxmx.rs index d71e28b7..59aff679 100644 --- a/src/snapshot_middleware/rbxmx.rs +++ b/src/snapshot_middleware/rbxmx.rs @@ -2,8 +2,12 @@ use std::path::Path; use anyhow::Context; use memofs::Vfs; +use rbx_xml::EncodeOptions; -use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; pub fn snapshot_rbxmx( context: &InstanceContext, @@ -15,7 +19,7 @@ pub fn snapshot_rbxmx( .property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown); let temp_tree = rbx_xml::from_reader(vfs.read(path)?.as_slice(), options) - .with_context(|| format!("Malformed rbxm file: {}", path.display()))?; + .with_context(|| format!("Malformed rbxmx file: {}", path.display()))?; let root_instance = temp_tree.root(); let children = root_instance.children(); @@ -41,6 +45,32 @@ pub fn snapshot_rbxmx( } } +pub fn syncback_rbxmx<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + let inst = snapshot.new_inst(); + + let options = + EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown); + + // Long-term, we probably want to have some logic for if this contains a + // script. That's a future endeavor though. + let mut serialized = Vec::new(); + rbx_xml::to_writer( + &mut serialized, + snapshot.new_tree(), + &[inst.referent()], + options, + ) + .context("failed to serialize new rbxmx")?; + + Ok(SyncbackReturn { + fs_snapshot: FsSnapshot::new().with_added_file(&snapshot.path, serialized), + children: Vec::new(), + removed_children: Vec::new(), + }) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap index 577f0f56..f8ce15af 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap index 39abe8ed..6aabdf4b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: root class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap index 75bf31bc..7202e31b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: root class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap index ede33710..8452cc7c 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: LocalizationTable properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap index 6a182d33..64f74c4e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Folder properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap index 3999f376..2f29bb64 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Folder properties: {} @@ -44,6 +45,7 @@ children: context: emit_legacy_scripts: true specified_id: ~ + middleware: dir name: Child class_name: Folder properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap index 69ae3c26..d570009f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap index 580b0b38..38d66519 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap index c6c30705..bf768977 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: IntValue properties: @@ -25,6 +26,7 @@ children: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: The Child class_name: StringValue properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap index c6c30705..bf768977 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: IntValue properties: @@ -25,6 +26,7 @@ children: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: The Child class_name: StringValue properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap index 49445676..683eec85 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: LocalScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap index d0a134da..b7ca7678 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap index ad129df9..63a84714 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap index 4b870d05..c4d02c8e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: bar class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap index 0dfb278a..916d0038 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap index 5007e665..a43f7e0f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap index 8a24827b..deb32404 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: root class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap index ea560ad1..b02f9117 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap @@ -21,6 +21,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: root class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap index 5f21eb7f..bf7b89b4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_module_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap index 507e25d6..e1376702 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap index 89ce9626..187ce50c 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap index b869da66..382fa03f 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap index 15523090..2d6d9bdb 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: bar class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap index 182e0fd3..2e68f9d8 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap index dc22b34a..c72bdfcd 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: false specified_id: ~ + middleware: ~ name: foo class_name: Script properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap index d9577829..f21cca30 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap index d9577829..f21cca30 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap index 4d2a9e55..08f7159d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap index 4d2a9e55..08f7159d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap @@ -11,6 +11,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: DEFAULT class_name: DEFAULT properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap index 7c1978ca..ebf5362d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap index 7906dbbf..1b87aab8 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: direct-project class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap index 51da0621..cd32e81d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap @@ -13,6 +13,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project name: path-property-override class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap index 0c9f90ab..f9d6e4a4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: children class_name: Folder properties: {} @@ -21,14 +22,16 @@ children: ignore_unknown_instances: true instigating_source: ProjectNode: - - /foo.project.json - - Child - - $className: Model - - Folder + path: /foo.project.json + name: Child + node: + $className: Model + parent_class: Folder relevant_paths: [] context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: Child class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap index d8d59d90..da28d1ce 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap @@ -13,6 +13,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project name: path-project class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap index 30b01ebb..077ebb15 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap @@ -13,6 +13,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: project name: path-child-project class_name: Folder properties: {} @@ -22,14 +23,16 @@ children: ignore_unknown_instances: true instigating_source: ProjectNode: - - /foo/other.project.json - - SomeChild - - $className: Model - - Folder + path: /foo/other.project.json + name: SomeChild + node: + $className: Model + parent_class: Folder relevant_paths: [] context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: SomeChild class_name: Model properties: {} diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap index 75320b53..ba9f0cf7 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap @@ -15,6 +15,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: text name: path-project class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap index 99c6b3b5..27d7004a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: resolved-properties class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap index f42e2246..c6349bae 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap @@ -12,6 +12,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: unresolved-properties class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap index e21e9414..22ae7577 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap index 4dd7cfc3..9905820d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap index bd877a12..769081e2 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap index c99e8910..684d8820 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: StringValue properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap index 33d2e1a4..a59d5fdd 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: ~ + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap index 4d04eac5..3afe180a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap @@ -14,6 +14,7 @@ metadata: context: emit_legacy_scripts: true specified_id: manually specified + middleware: ~ name: foo class_name: ModuleScript properties: diff --git a/src/snapshot_middleware/txt.rs b/src/snapshot_middleware/txt.rs index 761ba671..715354e0 100644 --- a/src/snapshot_middleware/txt.rs +++ b/src/snapshot_middleware/txt.rs @@ -1,11 +1,16 @@ 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}; +use crate::{ + snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot}, + syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot}, +}; -use super::meta_file::AdjacentMetadata; +use super::{meta_file::AdjacentMetadata, PathExt as _}; pub fn snapshot_txt( context: &InstanceContext, @@ -32,6 +37,41 @@ pub fn snapshot_txt( Ok(Some(snapshot)) } +pub fn syncback_txt<'sync>( + snapshot: &SyncbackSnapshot<'sync>, +) -> anyhow::Result> { + 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()?; + fs_snapshot.add_file( + parent.join(format!("{}.meta.json", new_inst.name)), + 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::*; diff --git a/src/snapshot_middleware/util.rs b/src/snapshot_middleware/util.rs index 625910b7..c16edcce 100644 --- a/src/snapshot_middleware/util.rs +++ b/src/snapshot_middleware/util.rs @@ -16,6 +16,7 @@ pub fn match_trailing<'a>(input: &'a str, suffix: &str) -> Option<&'a str> { pub trait PathExt { fn file_name_ends_with(&self, suffix: &str) -> bool; fn file_name_trim_end<'a>(&'a self, suffix: &str) -> anyhow::Result<&'a str>; + fn parent_err(&self) -> anyhow::Result<&Path>; } impl

PathExt for P @@ -40,6 +41,12 @@ where match_trailing(file_name, suffix) .with_context(|| format!("Path did not end in {}: {}", suffix, path.display())) } + + fn parent_err(&self) -> anyhow::Result<&Path> { + let path = self.as_ref(); + path.parent() + .with_context(|| format!("Path does not have a parent: {}", path.display())) + } } // TEMP function until rojo 8.0, when it can be replaced with bool::default (aka false) diff --git a/src/syncback/file_names.rs b/src/syncback/file_names.rs new file mode 100644 index 00000000..4653aabd --- /dev/null +++ b/src/syncback/file_names.rs @@ -0,0 +1,128 @@ +//! Contains logic for generating new file names for Instances based on their +//! middleware. + +use std::borrow::Cow; + +use anyhow::Context; +use rbx_dom_weak::Instance; + +use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware}; + +pub fn name_for_inst<'old>( + middleware: Middleware, + new_inst: &Instance, + old_inst: Option>, +) -> anyhow::Result> { + if let Some(old_inst) = old_inst { + if let Some(source) = old_inst.metadata().relevant_paths.first() { + source + .file_name() + .and_then(|s| s.to_str()) + .map(Cow::Borrowed) + .context("sources on the file system should be valid unicode and not be stubs") + } else { + // This is technically not /always/ true, but we want to avoid + // running syncback on anything that has no instigating source + // anyway. + anyhow::bail!( + "members of 'old' trees should have an instigating source. Somehow, {} did not.", + old_inst.name(), + ); + } + } else { + Ok(match middleware { + Middleware::Dir + | Middleware::CsvDir + | Middleware::ServerScriptDir + | Middleware::ClientScriptDir + | Middleware::ModuleScriptDir => Cow::Owned(new_inst.name.clone()), + _ => { + let extension = extension_for_middleware(middleware); + let name = &new_inst.name; + validate_file_name(name).with_context(|| { + format!("name '{name}' is not legal to write to the file system") + })?; + Cow::Owned(format!("{name}.{extension}")) + } + }) + } +} + +/// Returns the extension a provided piece of middleware is supposed to use. +pub fn extension_for_middleware(middleware: Middleware) -> &'static str { + match middleware { + Middleware::Csv => "csv", + Middleware::JsonModel => "model.json", + Middleware::Json => "json", + Middleware::ServerScript => "server.luau", + Middleware::ClientScript => "client.luau", + Middleware::ModuleScript => "luau", + Middleware::PluginScript => "plugin.luau", + Middleware::Project => "project.json", + Middleware::Rbxm => "rbxm", + Middleware::Rbxmx => "rbxmx", + Middleware::Toml => "toml", + Middleware::Text => "txt", + Middleware::Yaml => "yml", + + Middleware::LegacyServerScript + | Middleware::LegacyClientScript + | Middleware::RunContextServerScript + | Middleware::RunContextClientScript => { + todo!("syncback does not work on the middleware {middleware:?} yet") + } + // These are manually specified and not `_` to guard against future + // middleware additions missing this function. + Middleware::Ignore => unimplemented!("syncback does not work on Ignore middleware"), + Middleware::Dir + | Middleware::CsvDir + | Middleware::ServerScriptDir + | Middleware::ClientScriptDir + | Middleware::ModuleScriptDir => { + unimplemented!("directory middleware requires special treatment") + } + } +} + +/// A list of file names that are not valid on Windows. +const INVALID_WINDOWS_NAMES: [&str; 22] = [ + "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", + "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", +]; + +/// A list of all characters that are outright forbidden to be included +/// in a file's name. +const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\']; + +/// 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. +/// This takes into account rules for Windows, MacOS, and Linux. +/// +/// In practice however, these broadly overlap so the only unexpected behavior +/// is Windows, where there are 22 reserved names. +pub fn validate_file_name>(name: S) -> anyhow::Result<()> { + let str = name.as_ref(); + + if str.ends_with(' ') { + anyhow::bail!("file names cannot end with a space") + } + if str.ends_with('.') { + anyhow::bail!("file names cannot end with '.'") + } + + for char in str.chars() { + if FORBIDDEN_CHARS.contains(&char) { + anyhow::bail!("file names cannot contain <, >, :, \", /, |, ?, *, or \\") + } else if char.is_control() { + anyhow::bail!("file names cannot contain control characters") + } + } + + for forbidden in INVALID_WINDOWS_NAMES { + if str == forbidden { + anyhow::bail!("files cannot be named {str}") + } + } + + Ok(()) +} diff --git a/src/syncback/fs_snapshot.rs b/src/syncback/fs_snapshot.rs new file mode 100644 index 00000000..6bca52e4 --- /dev/null +++ b/src/syncback/fs_snapshot.rs @@ -0,0 +1,191 @@ +use std::{ + collections::{HashMap, HashSet}, + io, + path::{Path, PathBuf}, +}; + +use memofs::Vfs; + +/// A simple representation of a subsection of a file system. +#[derive(Default)] +pub struct FsSnapshot { + /// Paths representing new files mapped to their contents. + added_files: HashMap>, + /// Paths representing new directories. + added_dirs: HashSet, + /// Paths representing removed files. + removed_files: HashSet, + /// Paths representing removed directories. + removed_dirs: HashSet, +} + +impl FsSnapshot { + /// Creates a new `FsSnapshot`. + pub fn new() -> Self { + Self { + added_files: HashMap::new(), + added_dirs: HashSet::new(), + removed_files: HashSet::new(), + removed_dirs: HashSet::new(), + } + } + + /// Adds the given path to the `FsSnapshot` as a file with the given + /// contents, then returns it. + pub fn with_added_file>(mut self, path: P, data: Vec) -> Self { + self.added_files.insert(path.as_ref().to_path_buf(), data); + self + } + + /// Adds the given path to the `FsSnapshot` as a file with the given + /// then returns it. + pub fn with_added_dir>(mut self, path: P) -> Self { + self.added_dirs.insert(path.as_ref().to_path_buf()); + self + } + + /// Merges two `FsSnapshot`s together. + #[inline] + pub fn merge(&mut self, other: Self) { + self.added_files.extend(other.added_files); + self.added_dirs.extend(other.added_dirs); + self.removed_files.extend(other.removed_files); + self.removed_dirs.extend(other.removed_dirs); + } + + /// Merges two `FsSnapshot`s together, with a filter applied to the paths. + #[inline] + pub fn merge_with_filter(&mut self, other: Self, mut predicate: F) + where + F: FnMut(&Path) -> bool, + { + self.added_files + .extend(other.added_files.into_iter().filter(|(k, _)| predicate(k))); + self.added_dirs + .extend(other.added_dirs.into_iter().filter(|p| predicate(p))); + self.removed_files + .extend(other.removed_files.into_iter().filter(|p| predicate(p))); + self.removed_dirs + .extend(other.removed_dirs.into_iter().filter(|p| predicate(p))); + } + + /// Adds the provided path as a file with the given contents. + pub fn add_file>(&mut self, path: P, data: Vec) { + self.added_files.insert(path.as_ref().to_path_buf(), data); + } + + /// Adds the provided path as a directory. + pub fn add_dir>(&mut self, path: P) { + self.added_dirs.insert(path.as_ref().to_path_buf()); + } + + /// Removes the provided path, as a file. + pub fn remove_file>(&mut self, path: P) { + self.removed_files.insert(path.as_ref().to_path_buf()); + } + + /// Removes the provided path, as a directory. + pub fn remove_dir>(&mut self, path: P) { + self.removed_dirs.insert(path.as_ref().to_path_buf()); + } + + /// Writes the `FsSnapshot` to the provided VFS, using the provided `base` + /// as a root for the other paths in the `FsSnapshot`. + /// + /// This includes removals, but makes no effort to minimize work done. + pub fn write_to_vfs>(&self, base: P, vfs: &Vfs) -> io::Result<()> { + let mut lock = vfs.lock(); + + let base_path = base.as_ref(); + for dir_path in &self.added_dirs { + match lock.create_dir_all(base_path.join(dir_path)) { + Ok(_) => (), + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), + Err(err) => return Err(err), + }; + } + for (path, contents) in &self.added_files { + lock.write(base_path.join(path), contents)?; + } + for dir_path in &self.removed_dirs { + lock.remove_dir_all(base_path.join(dir_path))?; + } + for path in &self.removed_files { + lock.remove_file(base_path.join(path))?; + } + drop(lock); + + log::debug!( + "Wrote {} directories and {} files to the file system", + self.added_dirs.len(), + self.added_files.len() + ); + log::debug!( + "Removed {} directories and {} files from the file system", + self.removed_dirs.len(), + self.removed_files.len() + ); + Ok(()) + } + + /// Returns whether this `FsSnapshot` is empty or not. + #[inline] + pub fn is_empty(&self) -> bool { + self.added_files.is_empty() + && self.added_dirs.is_empty() + && self.removed_files.is_empty() + && self.removed_dirs.is_empty() + } + + /// Returns a list of paths that would be added by this `FsSnapshot`. + #[inline] + pub fn added_paths(&self) -> Vec<&Path> { + let mut list = Vec::with_capacity(self.added_files.len() + self.added_dirs.len()); + list.extend(self.added_files()); + list.extend(self.added_dirs()); + + list + } + + /// Returns a list of paths that would be removed by this `FsSnapshot`. + #[inline] + pub fn removed_paths(&self) -> Vec<&Path> { + let mut list = Vec::with_capacity(self.removed_files.len() + self.removed_dirs.len()); + list.extend(self.removed_files()); + list.extend(self.removed_dirs()); + + list + } + + /// Returns a list of file paths that would be added by this `FsSnapshot` + #[inline] + pub fn added_files(&self) -> Vec<&Path> { + let mut added_files: Vec<_> = self.added_files.keys().map(PathBuf::as_path).collect(); + added_files.sort_unstable(); + added_files + } + + /// Returns a list of directory paths that would be added by this `FsSnapshot` + #[inline] + pub fn added_dirs(&self) -> Vec<&Path> { + let mut added_dirs: Vec<_> = self.added_dirs.iter().map(PathBuf::as_path).collect(); + added_dirs.sort_unstable(); + added_dirs + } + + /// Returns a list of file paths that would be removed by this `FsSnapshot` + #[inline] + pub fn removed_files(&self) -> Vec<&Path> { + let mut removed_files: Vec<_> = self.removed_files.iter().map(PathBuf::as_path).collect(); + removed_files.sort_unstable(); + removed_files + } + + /// Returns a list of directory paths that would be removed by this `FsSnapshot` + #[inline] + pub fn removed_dirs(&self) -> Vec<&Path> { + let mut removed_dirs: Vec<_> = self.removed_dirs.iter().map(PathBuf::as_path).collect(); + removed_dirs.sort_unstable(); + removed_dirs + } +} diff --git a/src/syncback/hash/mod.rs b/src/syncback/hash/mod.rs new file mode 100644 index 00000000..5bc16b9f --- /dev/null +++ b/src/syncback/hash/mod.rs @@ -0,0 +1,122 @@ +//! Hashing utilities for a WeakDom. +mod variant; + +pub use variant::*; + +use blake3::{Hash, Hasher}; +use rbx_dom_weak::{ + types::{Ref, Variant}, + Instance, Ustr, WeakDom, +}; +use std::collections::HashMap; + +use crate::{variant_eq::variant_eq, Project}; + +use super::{descendants, filter_properties_preallocated}; + +/// Returns a map of every `Ref` in the `WeakDom` to a hashed version of the +/// `Instance` it points to, including the properties and descendants of the +/// `Instance`. +/// +/// The hashes **do** include the descendants of the Instances in them, +/// so they should only be used for comparing subtrees directly. +pub fn hash_tree(project: &Project, dom: &WeakDom, root_ref: Ref) -> HashMap { + let mut order = descendants(dom, root_ref); + let mut map: HashMap = HashMap::with_capacity(order.len()); + + let mut prop_list = Vec::with_capacity(2); + let mut child_hashes = Vec::new(); + + while let Some(referent) = order.pop() { + let inst = dom.get_by_ref(referent).unwrap(); + let mut hasher = hash_inst_filtered(project, inst, &mut prop_list); + add_children(inst, &map, &mut child_hashes, &mut hasher); + + map.insert(referent, hasher.finalize()); + } + + map +} + +/// Hashes a single Instance from the provided WeakDom, if it exists. +/// +/// This function filters properties using user-provided syncing rules from +/// the passed project. +#[inline] +pub fn hash_instance(project: &Project, dom: &WeakDom, referent: Ref) -> Option { + let mut prop_list = Vec::with_capacity(2); + let inst = dom.get_by_ref(referent)?; + + Some(hash_inst_filtered(project, inst, &mut prop_list).finalize()) +} + +/// Adds the hashes of children for an Instance to the provided Hasher. +fn add_children( + inst: &Instance, + map: &HashMap, + child_hashes: &mut Vec<[u8; 32]>, + hasher: &mut Hasher, +) { + for child_ref in inst.children() { + if let Some(hash) = map.get(child_ref) { + child_hashes.push(*hash.as_bytes()) + } else { + panic!("Invariant violated: child not hashed before parent") + } + } + child_hashes.sort_unstable(); + + for hash in child_hashes.drain(..) { + hasher.update(&hash); + } +} + +/// Performs hashing on an Instance using a filtered property list. +/// Does not include the hashes of any children. +fn hash_inst_filtered<'inst>( + project: &Project, + inst: &'inst Instance, + prop_list: &mut Vec<(Ustr, &'inst Variant)>, +) -> Hasher { + filter_properties_preallocated(project, inst, prop_list); + + hash_inst_prefilled(inst, prop_list) +} + +/// Performs hashing on an Instance using a pre-filled list of properties. +/// It is assumed the property list is **not** sorted, so it is sorted in-line. +fn hash_inst_prefilled<'inst>( + inst: &'inst Instance, + prop_list: &mut Vec<(Ustr, &'inst Variant)>, +) -> Hasher { + let mut hasher = Hasher::new(); + hasher.update(inst.name.as_bytes()); + hasher.update(inst.class.as_bytes()); + + prop_list.sort_unstable_by_key(|(name, _)| *name); + + let descriptor = rbx_reflection_database::get() + .unwrap() + .classes + .get(inst.class.as_str()); + + if let Some(descriptor) = descriptor { + for (name, value) in prop_list.drain(..) { + hasher.update(name.as_bytes()); + if let Some(default) = descriptor.default_properties.get(name.as_str()) { + if !variant_eq(default, value) { + hash_variant(&mut hasher, value) + } + } else { + hash_variant(&mut hasher, value) + } + } + } else { + for (name, value) in prop_list.drain(..) { + hasher.update(name.as_bytes()); + hash_variant(&mut hasher, value) + } + } + + hasher +} diff --git a/src/syncback/hash/variant.rs b/src/syncback/hash/variant.rs new file mode 100644 index 00000000..4e32bc33 --- /dev/null +++ b/src/syncback/hash/variant.rs @@ -0,0 +1,212 @@ +use blake3::Hasher; +use rbx_dom_weak::types::{ContentType, PhysicalProperties, Variant, Vector3}; + +macro_rules! round { + ($value:expr) => { + (($value * 10.0).round() / 10.0) + }; +} + +macro_rules! n_hash { + ($hash:ident, $($num:expr),*) => { + {$( + $hash.update(&($num).to_le_bytes()); + )*} + }; +} + +macro_rules! hash { + ($hash:ident, $value:expr) => {{ + $hash.update($value); + }}; +} + +/// Places `value` into the provided hasher. +pub fn hash_variant(hasher: &mut Hasher, value: &Variant) { + // We need to round floats, though I'm not sure to what degree we can + // realistically do that. + match value { + Variant::Attributes(attrs) => { + let mut sorted: Vec<(&String, &Variant)> = attrs.iter().collect(); + sorted.sort_unstable_by_key(|(name, _)| *name); + for (name, attribute) in sorted { + hasher.update(name.as_bytes()); + hash_variant(hasher, attribute); + } + } + Variant::Axes(a) => hash!(hasher, &[a.bits()]), + Variant::BinaryString(bytes) => hash!(hasher, bytes.as_ref()), + Variant::Bool(bool) => hash!(hasher, &[*bool as u8]), + Variant::BrickColor(color) => n_hash!(hasher, *color as u16), + Variant::CFrame(cf) => { + vector_hash(hasher, cf.position); + vector_hash(hasher, cf.orientation.x); + vector_hash(hasher, cf.orientation.y); + vector_hash(hasher, cf.orientation.z); + } + Variant::Color3(color) => { + n_hash!(hasher, round!(color.r), round!(color.g), round!(color.b)) + } + Variant::Color3uint8(color) => hash!(hasher, &[color.r, color.b, color.g]), + Variant::ColorSequence(seq) => { + let mut new = Vec::with_capacity(seq.keypoints.len()); + for keypoint in &seq.keypoints { + new.push(keypoint); + } + new.sort_unstable_by(|a, b| round!(a.time).partial_cmp(&round!(b.time)).unwrap()); + for keypoint in new { + n_hash!( + hasher, + round!(keypoint.time), + round!(keypoint.color.r), + round!(keypoint.color.g), + round!(keypoint.color.b) + ) + } + } + Variant::Content(content) => match content.value() { + ContentType::None => { + hash!(hasher, &[0]); + } + ContentType::Uri(uri) => { + hash!(hasher, &[1]); + hash!(hasher, uri.as_bytes()); + } + ContentType::Object(referent) => { + hash!(hasher, &[2]); + hash!(hasher, referent.to_string().as_bytes()) + } + other => { + panic!("the ContentType {other:?} cannot be hashed as a Variant") + } + }, + Variant::ContentId(content) => { + let s: &str = content.as_ref(); + hash!(hasher, s.as_bytes()) + } + Variant::Enum(e) => n_hash!(hasher, e.to_u32()), + Variant::Faces(f) => hash!(hasher, &[f.bits()]), + Variant::Float32(n) => n_hash!(hasher, round!(*n)), + Variant::Float64(n) => n_hash!(hasher, round!(n)), + Variant::Font(f) => { + n_hash!(hasher, f.weight as u16); + n_hash!(hasher, f.style as u8); + hash!(hasher, f.family.as_bytes()); + if let Some(cache) = &f.cached_face_id { + hash!(hasher, &[0x01]); + hash!(hasher, cache.as_bytes()); + } else { + hash!(hasher, &[0x00]); + } + } + Variant::Int32(n) => n_hash!(hasher, n), + Variant::Int64(n) => n_hash!(hasher, n), + Variant::MaterialColors(n) => hash!(hasher, n.encode().as_slice()), + Variant::NetAssetRef(net_asset) => hash!(hasher, net_asset.hash().as_bytes()), + Variant::NumberRange(nr) => n_hash!(hasher, round!(nr.max), round!(nr.min)), + Variant::NumberSequence(seq) => { + let mut new = Vec::with_capacity(seq.keypoints.len()); + for keypoint in &seq.keypoints { + new.push(keypoint); + } + new.sort_unstable_by(|a, b| round!(a.time).partial_cmp(&round!(b.time)).unwrap()); + for keypoint in new { + n_hash!( + hasher, + round!(keypoint.time), + round!(keypoint.value), + round!(keypoint.envelope) + ) + } + } + Variant::OptionalCFrame(maybe_cf) => { + if let Some(cf) = maybe_cf { + hash!(hasher, &[0x01]); + vector_hash(hasher, cf.position); + vector_hash(hasher, cf.orientation.x); + vector_hash(hasher, cf.orientation.y); + vector_hash(hasher, cf.orientation.z); + } else { + hash!(hasher, &[0x00]); + } + } + Variant::PhysicalProperties(properties) => match properties { + PhysicalProperties::Default => hash!(hasher, &[0x00]), + PhysicalProperties::Custom(custom) => { + hash!(hasher, &[0x00]); + n_hash!( + hasher, + round!(custom.density()), + round!(custom.friction()), + round!(custom.elasticity()), + round!(custom.friction_weight()), + round!(custom.elasticity_weight()), + round!(custom.acoustic_absorption()) + ) + } + }, + Variant::Ray(ray) => { + vector_hash(hasher, ray.origin); + vector_hash(hasher, ray.direction); + } + Variant::Rect(rect) => n_hash!( + hasher, + round!(rect.max.x), + round!(rect.max.y), + round!(rect.min.x), + round!(rect.min.y) + ), + Variant::Ref(referent) => hash!(hasher, referent.to_string().as_bytes()), + Variant::Region3(region) => { + vector_hash(hasher, region.max); + vector_hash(hasher, region.min); + } + Variant::Region3int16(region) => { + n_hash!( + hasher, + region.max.x, + region.max.y, + region.max.z, + region.min.x, + region.min.y, + region.min.z + ) + } + Variant::SecurityCapabilities(capabilities) => n_hash!(hasher, capabilities.bits()), + Variant::SharedString(sstr) => hash!(hasher, sstr.hash().as_bytes()), + Variant::String(str) => hash!(hasher, str.as_bytes()), + Variant::Tags(tags) => { + let mut dupe: Vec<&str> = tags.iter().collect(); + dupe.sort_unstable(); + for tag in dupe { + hash!(hasher, tag.as_bytes()) + } + } + Variant::UDim(udim) => n_hash!(hasher, round!(udim.scale), udim.offset), + Variant::UDim2(udim) => n_hash!( + hasher, + round!(udim.y.scale), + udim.y.offset, + round!(udim.x.scale), + udim.x.offset + ), + Variant::Vector2(v2) => n_hash!(hasher, round!(v2.x), round!(v2.y)), + Variant::Vector2int16(v2) => n_hash!(hasher, v2.x, v2.y), + Variant::Vector3(v3) => vector_hash(hasher, *v3), + Variant::Vector3int16(v3) => n_hash!(hasher, v3.x, v3.y, v3.z), + + // Hashing UniqueId properties doesn't make sense + Variant::UniqueId(_) => (), + + unknown => { + log::warn!( + "Encountered unknown Variant {:?} while hashing", + unknown.ty() + ) + } + } +} + +fn vector_hash(hasher: &mut Hasher, vector: Vector3) { + n_hash!(hasher, round!(vector.x), round!(vector.y), round!(vector.z)) +} diff --git a/src/syncback/mod.rs b/src/syncback/mod.rs new file mode 100644 index 00000000..61d26674 --- /dev/null +++ b/src/syncback/mod.rs @@ -0,0 +1,534 @@ +mod file_names; +mod fs_snapshot; +mod hash; +mod property_filter; +mod ref_properties; +mod snapshot; + +use anyhow::Context; +use indexmap::IndexMap; +use memofs::Vfs; +use rbx_dom_weak::{ + types::{Ref, Variant}, + ustr, Instance, Ustr, UstrSet, WeakDom, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + env, + path::Path, + sync::OnceLock, +}; + +use crate::{ + glob::Glob, + snapshot::{InstanceWithMeta, RojoTree}, + snapshot_middleware::Middleware, + syncback::ref_properties::{collect_referents, link_referents}, + Project, +}; + +pub use file_names::{extension_for_middleware, name_for_inst, validate_file_name}; +pub use fs_snapshot::FsSnapshot; +pub use hash::*; +pub use property_filter::{filter_properties, filter_properties_preallocated}; +pub use snapshot::{SyncbackData, SyncbackSnapshot}; + +/// The name of an enviroment variable to use to override the behavior of +/// syncback on model files. +/// By default, syncback will use `Rbxm` for model files. +/// If this is set to `1`, it will instead use `Rbxmx`. If it is set to `2`, +/// it will use `JsonModel`. +/// +/// This will **not** override existing `Rbxm` middleware. It will only impact +/// new files. +const DEBUG_MODEL_FORMAT_VAR: &str = "ROJO_SYNCBACK_DEBUG"; + +/// A glob that can be used to tell if a path contains a `.git` folder. +static GIT_IGNORE_GLOB: OnceLock = OnceLock::new(); + +pub fn syncback_loop( + vfs: &Vfs, + old_tree: &mut RojoTree, + mut new_tree: WeakDom, + project: &Project, +) -> anyhow::Result { + let ignore_patterns = project + .syncback_rules + .as_ref() + .map(|rules| rules.compile_globs()) + .transpose()?; + + // TODO: Add a better way to tell if the root of a project is a directory + let skip_pruning = if let Some(path) = &project.tree.path { + let middleware = + Middleware::middleware_for_path(vfs, &project.sync_rules, path.path()).unwrap(); + if let Some(middleware) = middleware { + middleware.is_dir() + } else { + false + } + } else { + false + }; + if !skip_pruning { + // Strip out any objects from the new tree that aren't in the old tree. This + // is necessary so that hashing the roots of each tree won't always result + // in different hashes. Shout out to Roblox for serializing a bunch of + // Services nobody cares about. + log::debug!("Pruning new tree"); + strip_unknown_root_children(&mut new_tree, old_tree); + } + + log::debug!("Collecting referents for new DOM..."); + let deferred_referents = collect_referents(&new_tree); + + // Remove any properties that are manually blocked from syncback via the + // project file. + log::debug!("Pre-filtering properties on DOMs"); + for referent in descendants(&new_tree, new_tree.root_ref()) { + let new_inst = new_tree.get_by_ref_mut(referent).unwrap(); + if let Some(filter) = get_property_filter(project, new_inst) { + for prop in filter { + new_inst.properties.remove(&prop); + } + } + } + for referent in descendants(old_tree.inner(), old_tree.get_root_id()) { + let mut old_inst_rojo = old_tree.get_instance_mut(referent).unwrap(); + let old_inst = old_inst_rojo.inner_mut(); + if let Some(filter) = get_property_filter(project, old_inst) { + for prop in filter { + old_inst.properties.remove(&prop); + } + } + } + + // Handle removing the current camera. + if let Some(syncback_rules) = &project.syncback_rules { + if !syncback_rules.sync_current_camera.unwrap_or_default() { + log::debug!("Removing CurrentCamera from new DOM"); + let mut camera_ref = None; + for child_ref in new_tree.root().children() { + let inst = new_tree.get_by_ref(*child_ref).unwrap(); + if inst.class == "Workspace" { + camera_ref = inst.properties.get(&ustr("CurrentCamera")); + break; + } + } + if let Some(Variant::Ref(camera_ref)) = camera_ref { + if new_tree.get_by_ref(*camera_ref).is_some() { + new_tree.destroy(*camera_ref); + } + } + } + } + + let ignore_referents = project + .syncback_rules + .as_ref() + .and_then(|s| s.ignore_referents) + .unwrap_or_default(); + if !ignore_referents { + log::debug!("Linking referents for new DOM"); + link_referents(deferred_referents, &mut new_tree)?; + } else { + log::debug!("Skipping referent linking as per project syncback rules"); + } + + // As with pruning the children of the new root, we need to ensure the roots + // for both DOMs have the same name otherwise their hashes will always be + // different. + new_tree.root_mut().name = old_tree.root().name().to_string(); + + log::debug!("Hashing project DOM"); + let old_hashes = hash_tree(project, old_tree.inner(), old_tree.get_root_id()); + log::debug!("Hashing file DOM"); + let new_hashes = hash_tree(project, &new_tree, new_tree.root_ref()); + + let project_path = project.folder_location(); + + let syncback_data = SyncbackData { + vfs, + old_tree, + new_tree: &new_tree, + project, + }; + + let mut snapshots = vec![SyncbackSnapshot { + data: syncback_data, + old: Some(old_tree.get_root_id()), + new: new_tree.root_ref(), + path: project.file_location.clone(), + middleware: Some(Middleware::Project), + }]; + + let mut fs_snapshot = FsSnapshot::new(); + + 'syncback: while let Some(snapshot) = snapshots.pop() { + let inst_path = snapshot.get_new_inst_path(snapshot.new); + // We can quickly check that two subtrees are identical and if they are, + // skip reconciling them. + if let Some(old_ref) = snapshot.old { + match (old_hashes.get(&old_ref), new_hashes.get(&snapshot.new)) { + (Some(old), Some(new)) => { + if old == new { + log::trace!( + "Skipping {inst_path} due to it being identically hashed as {old:?}" + ); + continue; + } + } + _ => unreachable!("All Instances in both DOMs should have hashes"), + } + } + + if !is_valid_path(&ignore_patterns, project_path, &snapshot.path) { + log::debug!("Skipping {inst_path} because its path matches ignore pattern"); + continue; + } + if let Some(syncback_rules) = &project.syncback_rules { + // Ignore trees; + for ignored in &syncback_rules.ignore_trees { + if inst_path.starts_with(ignored.as_str()) { + log::debug!("Tree {inst_path} is blocked by project"); + continue 'syncback; + } + } + } + + let middleware = get_best_middleware(&snapshot); + + log::trace!( + "Middleware for {inst_path} is {:?} (path is {})", + middleware, + snapshot.path.display() + ); + + if matches!(middleware, Middleware::Json | Middleware::Toml) { + log::warn!("Cannot syncback {middleware:?} at {inst_path}, skipping"); + continue; + } + + let syncback = match middleware.syncback(&snapshot) { + Ok(syncback) => syncback, + Err(err) if middleware == Middleware::Dir => { + let new_middleware = 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 + .path + .file_name() + .and_then(|s| s.to_str()) + .context("Directory middleware should have a name in its path")?; + let mut path = snapshot.path.clone(); + path.set_file_name(format!( + "{file_name}.{}", + extension_for_middleware(new_middleware) + )); + let new_snapshot = snapshot.with_new_path(path, snapshot.new, snapshot.old); + log::warn!( + "Could not syncback {inst_path} as a Directory because: {err}.\n\ + It will instead be synced back as a {new_middleware:?}." + ); + let new_syncback_result = new_middleware + .syncback(&new_snapshot) + .with_context(|| format!("Failed to syncback {inst_path}")); + if new_syncback_result.is_ok() && snapshot.old_inst().is_some() { + // We need to remove the old FS representation if we're + // reserializing it as an rbxm. + fs_snapshot.remove_dir(&snapshot.path); + } + new_syncback_result? + } + Err(err) => anyhow::bail!("Failed to syncback {inst_path} because {err}"), + }; + + if !syncback.removed_children.is_empty() { + log::debug!( + "removed children for {inst_path}: {}", + syncback.removed_children.len() + ); + 'remove: for inst in &syncback.removed_children { + let path = inst.metadata().instigating_source.as_ref().unwrap().path(); + let inst_path = snapshot.get_old_inst_path(inst.id()); + if !is_valid_path(&ignore_patterns, project_path, path) { + log::debug!( + "Skipping removing {} because its matches an ignore pattern", + path.display() + ); + continue; + } + if let Some(syncback_rules) = &project.syncback_rules { + for ignored in &syncback_rules.ignore_trees { + if inst_path.starts_with(ignored.as_str()) { + log::debug!("Skipping removing {inst_path} because its path is blocked by project"); + continue 'remove; + } + } + } + if path.is_dir() { + fs_snapshot.remove_dir(path) + } else { + fs_snapshot.remove_file(path) + } + } + } + + // TODO provide replacement snapshots for e.g. two way sync + + fs_snapshot.merge_with_filter(syncback.fs_snapshot, |path| { + is_valid_path(&ignore_patterns, project_path, path) + }); + + snapshots.extend(syncback.children); + } + + Ok(fs_snapshot) +} + +pub struct SyncbackReturn<'sync> { + pub fs_snapshot: FsSnapshot, + pub children: Vec>, + pub removed_children: Vec>, +} + +pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware { + // At some point, we're better off using an O(1) method for checking + // equality for classes like this. + static JSON_MODEL_CLASSES: OnceLock> = OnceLock::new(); + let json_model_classes = JSON_MODEL_CLASSES.get_or_init(|| { + [ + "Sound", + "SoundGroup", + "Sky", + "Atmosphere", + "BloomEffect", + "BlurEffect", + "ColorCorrectionEffect", + "DepthOfFieldEffect", + "SunRaysEffect", + "ParticleEmitter", + "TextChannel", + "TextChatCommand", + // TODO: Implement a way to use inheritance for this + "ChatWindowConfiguration", + "ChatInputBarConfiguration", + "BubbleChatConfiguration", + "ChannelTabsConfiguration", + ] + .into() + }); + + let old_middleware = snapshot + .old_inst() + .and_then(|inst| inst.metadata().middleware); + let inst = snapshot.new_inst(); + + let mut middleware; + + if let Some(override_middleware) = snapshot.middleware { + return override_middleware; + } else if let Some(old_middleware) = old_middleware { + return old_middleware; + } else if json_model_classes.contains(inst.class.as_str()) { + middleware = Middleware::JsonModel; + } else { + middleware = match inst.class.as_str() { + "Folder" | "Configuration" | "Tool" => Middleware::Dir, + "StringValue" => Middleware::Text, + "Script" => Middleware::ServerScript, + "LocalScript" => Middleware::ClientScript, + "ModuleScript" => Middleware::ModuleScript, + "LocalizationTable" => Middleware::Csv, + // This isn't the ideal way to handle this but it works. + name if name.ends_with("Value") => Middleware::JsonModel, + _ => Middleware::Rbxm, + } + } + + if !inst.children().is_empty() { + middleware = match middleware { + Middleware::ServerScript => Middleware::ServerScriptDir, + Middleware::ClientScript => Middleware::ClientScriptDir, + Middleware::ModuleScript => Middleware::ModuleScriptDir, + Middleware::Csv => Middleware::CsvDir, + Middleware::JsonModel | Middleware::Text => Middleware::Dir, + _ => middleware, + } + } + + if middleware == Middleware::Rbxm { + middleware = match env::var(DEBUG_MODEL_FORMAT_VAR) { + Ok(value) if value == "1" => Middleware::Rbxmx, + Ok(value) if value == "2" => Middleware::JsonModel, + _ => Middleware::Rbxm, + } + } + + middleware +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct SyncbackRules { + /// A list of subtrees in a file that will be ignored by Syncback. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + ignore_trees: Vec, + /// A list of patterns to check against the path an Instance would serialize + /// to. If a path matches one of these, the Instance won't be syncbacked. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + ignore_paths: Vec, + /// A map of classes to properties to ignore for that class when doing + /// syncback. + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + ignore_properties: IndexMap>, + /// Whether or not the `CurrentCamera` of `Workspace` is included in the + /// syncback or not. Defaults to `false`. + #[serde(skip_serializing_if = "Option::is_none")] + sync_current_camera: Option, + /// Whether or not to sync properties that cannot be modified via scripts. + /// Defaults to `true`. + #[serde(skip_serializing_if = "Option::is_none")] + sync_unscriptable: Option, + /// Whether to skip serializing referent properties like `Model.PrimaryPart` + /// during syncback. Defaults to `false`. + #[serde(skip_serializing_if = "Option::is_none")] + ignore_referents: Option, + /// Whether the globs specified in `ignore_paths` should be modified to also + /// match directories. Defaults to `true`. + /// + /// If this is `true`, it'll take ignore globs that end in `/**` and convert + /// them to also handle the directory they're referring to. This is + /// generally a better UX. + #[serde(skip_serializing_if = "Option::is_none")] + create_ignore_dir_paths: Option, +} + +impl SyncbackRules { + pub fn compile_globs(&self) -> anyhow::Result> { + let mut globs = Vec::with_capacity(self.ignore_paths.len()); + let dir_ignore_paths = self.create_ignore_dir_paths.unwrap_or(true); + + for pattern in &self.ignore_paths { + let glob = Glob::new(pattern) + .with_context(|| format!("the pattern '{pattern}' is not a valid glob"))?; + globs.push(glob); + + if dir_ignore_paths { + if let Some(dir_pattern) = pattern.strip_suffix("/**") { + if let Ok(glob) = Glob::new(dir_pattern) { + globs.push(glob) + } + } + } + } + + Ok(globs) + } +} + +fn is_valid_path(globs: &Option>, base_path: &Path, path: &Path) -> bool { + let git_glob = GIT_IGNORE_GLOB.get_or_init(|| Glob::new(".git/**").unwrap()); + let test_path = match path.strip_prefix(base_path) { + Ok(suffix) => suffix, + Err(_) => path, + }; + if git_glob.is_match(test_path) { + return false; + } + if let Some(ref ignore_paths) = globs { + for glob in ignore_paths { + if glob.is_match(test_path) { + return false; + } + } + } + true +} + +/// Returns a set of properties that should not be written with syncback if +/// one exists. This list is read directly from the Project and takes +/// inheritance into effect. +/// +/// It **does not** handle properties that should not serialize for other +/// reasons, such as being defaults or being marked as not serializing in the +/// ReflectionDatabase. +fn get_property_filter(project: &Project, new_inst: &Instance) -> Option { + let filter = &project.syncback_rules.as_ref()?.ignore_properties; + let mut set = UstrSet::default(); + + let database = rbx_reflection_database::get().unwrap(); + let mut current_class_name = new_inst.class.as_str(); + + loop { + if let Some(list) = filter.get(&ustr(current_class_name)) { + set.extend(list) + } + + let class = database.classes.get(current_class_name)?; + if let Some(super_class) = class.superclass.as_ref() { + current_class_name = super_class; + } else { + break; + } + } + + Some(set) +} + +/// Produces a list of descendants in the WeakDom such that all children come +/// before their parents. +fn descendants(dom: &WeakDom, root_ref: Ref) -> Vec { + let mut queue = VecDeque::new(); + let mut ordered = Vec::new(); + queue.push_front(root_ref); + + while let Some(referent) = queue.pop_front() { + let inst = dom + .get_by_ref(referent) + .expect("Invariant: WeakDom had a Ref that wasn't inside it"); + ordered.push(referent); + for child in inst.children() { + queue.push_back(*child) + } + } + + ordered +} + +/// Removes the children of `new`'s root that are not also children of `old`'s +/// root. +/// +/// This does not care about duplicates, and only filters based on names and +/// class names. +fn strip_unknown_root_children(new: &mut WeakDom, old: &RojoTree) { + let old_root = old.root(); + let old_root_children: HashMap<&str, InstanceWithMeta> = old_root + .children() + .iter() + .map(|referent| { + let inst = old + .get_instance(*referent) + .expect("all children of a DOM's root should exist"); + (inst.name(), inst) + }) + .collect(); + + let root_children = new.root().children().to_vec(); + + for child_ref in root_children { + let child = new + .get_by_ref(child_ref) + .expect("all children of the root should exist in the DOM"); + if let Some(old) = old_root_children.get(child.name.as_str()) { + if old.class_name() == child.class { + continue; + } + } + log::trace!("Pruning root child {} of class {}", child.name, child.class); + new.destroy(child_ref); + } +} diff --git a/src/syncback/property_filter.rs b/src/syncback/property_filter.rs new file mode 100644 index 00000000..47678870 --- /dev/null +++ b/src/syncback/property_filter.rs @@ -0,0 +1,111 @@ +use rbx_dom_weak::{types::Variant, Instance, Ustr, UstrMap}; +use rbx_reflection::{PropertyKind, PropertySerialization, Scriptability}; + +use crate::{variant_eq::variant_eq, Project}; + +/// Returns a map of properties from `inst` that are both allowed under the +/// user-provided settings, are not their default value, and serialize. +pub fn filter_properties<'inst>( + project: &Project, + inst: &'inst Instance, +) -> UstrMap<&'inst Variant> { + let mut map: Vec<(Ustr, &Variant)> = Vec::with_capacity(inst.properties.len()); + filter_properties_preallocated(project, inst, &mut map); + + map.into_iter().collect() +} + +/// Fills `allocation` with a list of properties from `inst` that are +/// user-provided settings, are not their default value, and serialize. +pub fn filter_properties_preallocated<'inst>( + project: &Project, + inst: &'inst Instance, + allocation: &mut Vec<(Ustr, &'inst Variant)>, +) { + let sync_unscriptable = project + .syncback_rules + .as_ref() + .and_then(|s| s.sync_unscriptable) + .unwrap_or(true); + + let class_data = rbx_reflection_database::get() + .unwrap() + .classes + .get(inst.class.as_str()); + + let predicate = |prop_name: &Ustr, prop_value: &Variant| { + // We don't want to serialize Ref or UniqueId properties in JSON files + if matches!(prop_value, Variant::Ref(_) | Variant::UniqueId(_)) { + return true; + } + if !should_property_serialize(&inst.class, prop_name) { + return true; + } + if !sync_unscriptable { + if let Some(data) = class_data { + if let Some(prop_data) = data.properties.get(prop_name.as_str()) { + if matches!(prop_data.scriptability, Scriptability::None) { + return true; + } + } + } + } + false + }; + + if let Some(class_data) = class_data { + let defaults = &class_data.default_properties; + for (name, value) in &inst.properties { + if predicate(name, value) { + continue; + } + if let Some(default) = defaults.get(name.as_str()) { + if !variant_eq(value, default) { + allocation.push((*name, value)); + } + } else { + allocation.push((*name, value)); + } + } + } else { + for (name, value) in &inst.properties { + if predicate(name, value) { + continue; + } + allocation.push((*name, value)); + } + } +} + +fn should_property_serialize(class_name: &str, prop_name: &str) -> bool { + let database = rbx_reflection_database::get().unwrap(); + let mut current_class_name = class_name; + + loop { + let class_data = match database.classes.get(current_class_name) { + Some(data) => data, + None => return true, + }; + if let Some(data) = class_data.properties.get(prop_name) { + log::trace!("found {class_name}.{prop_name} on {current_class_name}"); + return match &data.kind { + // It's not really clear if this can ever happen but I want to + // support it just in case! + PropertyKind::Alias { alias_for } => { + should_property_serialize(current_class_name, alias_for) + } + // Migrations and aliases are happily handled for us by parsers + // so we don't really need to handle them. + PropertyKind::Canonical { serialization } => { + !matches!(serialization, PropertySerialization::DoesNotSerialize) + } + kind => unimplemented!("unknown property kind {kind:?}"), + }; + } else if let Some(super_class) = class_data.superclass.as_ref() { + current_class_name = super_class; + } else { + break; + } + } + true +} diff --git a/src/syncback/ref_properties.rs b/src/syncback/ref_properties.rs new file mode 100644 index 00000000..4a8385f8 --- /dev/null +++ b/src/syncback/ref_properties.rs @@ -0,0 +1,192 @@ +//! Implements iterating through an entire WeakDom and linking all Ref +//! properties using attributes. + +use std::collections::{HashMap, HashSet, VecDeque}; + +use rbx_dom_weak::{ + types::{Attributes, Ref, UniqueId, Variant}, + ustr, Instance, Ustr, WeakDom, +}; + +use crate::{multimap::MultiMap, REF_ID_ATTRIBUTE_NAME, REF_POINTER_ATTRIBUTE_PREFIX}; + +pub struct RefLinks { + /// A map of referents to each of their Ref properties. + prop_links: MultiMap, + /// A set of referents that need their ID rewritten. This includes + /// Instances that have no existing ID. + need_rewrite: HashSet, +} + +#[derive(PartialEq, Eq)] +struct RefLink { + /// The name of a property + name: Ustr, + /// The value of the property. + value: Ref, +} + +/// Iterates through a WeakDom and collects referent properties. +/// +/// They can be linked to a dom later using `link_referents`. +pub fn collect_referents(dom: &WeakDom) -> RefLinks { + let mut ids = HashMap::new(); + let mut need_rewrite = HashSet::new(); + let mut links = MultiMap::new(); + + // Note that this is back-in, front-out. This is important because + // VecDeque::extend is the equivalent to using push_back. + let mut queue = VecDeque::new(); + queue.push_back(dom.root_ref()); + while let Some(inst_ref) = queue.pop_front() { + let pointer = dom.get_by_ref(inst_ref).unwrap(); + queue.extend(pointer.children().iter().copied()); + + for (prop_name, prop_value) in &pointer.properties { + let Variant::Ref(prop_value) = prop_value else { + continue; + }; + if prop_value.is_none() { + continue; + } + + links.insert( + inst_ref, + RefLink { + name: *prop_name, + value: *prop_value, + }, + ); + + let target = dom + .get_by_ref(*prop_value) + .expect("Refs in DOM should point to valid Instances"); + + // 1. Check if target has an ID + if let Some(id) = get_existing_id(target) { + // If it does, we need to check whether that ID is a duplicate + if let Some(id_ref) = ids.get(id) { + // If the same ID points to a new Instance, rewrite it. + if id_ref != prop_value { + if log::log_enabled!(log::Level::Trace) { + log::trace!( + "{} needs an id rewritten because it has the same id as {}", + target.name, + dom.get_by_ref(*id_ref).unwrap().name + ); + } + need_rewrite.insert(*prop_value); + } + } + ids.insert(id, *prop_value); + } else { + log::trace!("{} needs an id rewritten because it has no id but is referred to by {}.{prop_name}", target.name, pointer.name); + // If it does not, it needs one. + need_rewrite.insert(*prop_value); + } + } + } + + RefLinks { + need_rewrite, + prop_links: links, + } +} + +pub fn link_referents(links: RefLinks, dom: &mut WeakDom) -> anyhow::Result<()> { + write_id_attributes(&links, dom)?; + + let mut prop_list = Vec::new(); + + for (inst_id, properties) in links.prop_links { + for ref_link in properties { + let prop_inst = match dom.get_by_ref(ref_link.value) { + Some(inst) => inst, + None => continue, + }; + let id = get_existing_id(prop_inst) + .expect("all Instances that are pointed to should have an ID"); + prop_list.push((ref_link.name, Variant::String(id.to_owned()))); + } + let inst = match dom.get_by_ref_mut(inst_id) { + Some(inst) => inst, + None => continue, + }; + + let mut attributes: Attributes = match inst.properties.remove(&ustr("Attributes")) { + Some(Variant::Attributes(attrs)) => attrs, + None => Attributes::new(), + Some(value) => { + anyhow::bail!( + "expected Attributes to be of type 'Attributes' but it was of type '{:?}'", + value.ty() + ); + } + } + .into_iter() + .filter(|(name, _)| !name.starts_with(REF_POINTER_ATTRIBUTE_PREFIX)) + .collect(); + + for (prop_name, prop_value) in prop_list.drain(..) { + attributes.insert( + format!("{REF_POINTER_ATTRIBUTE_PREFIX}{prop_name}"), + prop_value, + ); + } + + inst.properties + .insert("Attributes".into(), attributes.into()); + } + + Ok(()) +} + +fn write_id_attributes(links: &RefLinks, dom: &mut WeakDom) -> anyhow::Result<()> { + for referent in &links.need_rewrite { + let inst = match dom.get_by_ref_mut(*referent) { + Some(inst) => inst, + None => continue, + }; + let unique_id = match inst.properties.get(&ustr("UniqueId")) { + Some(Variant::UniqueId(id)) => Some(*id), + _ => None, + } + .unwrap_or_else(|| UniqueId::now().unwrap()); + + let attributes = match inst.properties.get_mut(&ustr("Attributes")) { + Some(Variant::Attributes(attrs)) => attrs, + None => { + inst.properties + .insert("Attributes".into(), Attributes::new().into()); + match inst.properties.get_mut(&ustr("Attributes")) { + Some(Variant::Attributes(attrs)) => attrs, + _ => unreachable!(), + } + } + Some(value) => { + anyhow::bail!( + "expected Attributes to be of type 'Attributes' but it was of type '{:?}'", + value.ty() + ); + } + }; + attributes.insert( + REF_ID_ATTRIBUTE_NAME.into(), + Variant::String(unique_id.to_string()), + ); + } + Ok(()) +} + +fn get_existing_id(inst: &Instance) -> Option<&str> { + if let Variant::Attributes(attrs) = inst.properties.get(&ustr("Attributes"))? { + let id = attrs.get(REF_ID_ATTRIBUTE_NAME)?; + match id { + Variant::String(str) => Some(str), + Variant::BinaryString(bstr) => std::str::from_utf8(bstr.as_ref()).ok(), + _ => None, + } + } else { + None + } +} diff --git a/src/syncback/snapshot.rs b/src/syncback/snapshot.rs new file mode 100644 index 00000000..384033d6 --- /dev/null +++ b/src/syncback/snapshot.rs @@ -0,0 +1,259 @@ +use indexmap::IndexMap; +use memofs::Vfs; +use std::path::{Path, PathBuf}; + +use crate::{ + snapshot::{InstanceWithMeta, RojoTree}, + snapshot_middleware::Middleware, + Project, +}; +use rbx_dom_weak::{ + types::{Ref, Variant}, + Instance, Ustr, UstrMap, WeakDom, +}; + +use super::{get_best_middleware, name_for_inst, property_filter::filter_properties}; + +#[derive(Clone, Copy)] +pub struct SyncbackData<'sync> { + pub(super) vfs: &'sync Vfs, + pub(super) old_tree: &'sync RojoTree, + pub(super) new_tree: &'sync WeakDom, + pub(super) project: &'sync Project, +} + +pub struct SyncbackSnapshot<'sync> { + pub data: SyncbackData<'sync>, + pub old: Option, + pub new: Ref, + pub path: PathBuf, + pub middleware: Option, +} + +impl<'sync> SyncbackSnapshot<'sync> { + /// Constructs a SyncbackSnapshot from the provided refs + /// while inheriting this snapshot's path and data. This should be used for + /// directories. + #[inline] + pub fn with_joined_path(&self, new_ref: Ref, old_ref: Option) -> anyhow::Result { + let mut snapshot = Self { + data: self.data, + old: old_ref, + new: new_ref, + path: PathBuf::new(), + middleware: None, + }; + let middleware = get_best_middleware(&snapshot); + let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?; + snapshot.path = self.path.join(name.as_ref()); + + Ok(snapshot) + } + + /// Constructs a SyncbackSnapshot from the provided refs and a base path, + /// while inheriting this snapshot's data. + /// + /// The actual path of the snapshot is made by getting a file name for the + /// snapshot and then appending it to the provided base path. + #[inline] + pub fn with_base_path( + &self, + base_path: &Path, + new_ref: Ref, + old_ref: Option, + ) -> anyhow::Result { + let mut snapshot = Self { + data: self.data, + old: old_ref, + new: new_ref, + path: PathBuf::new(), + middleware: None, + }; + let middleware = get_best_middleware(&snapshot); + let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?; + snapshot.path = base_path.join(name.as_ref()); + + Ok(snapshot) + } + + /// Constructs a SyncbackSnapshot with the provided path and refs while + /// inheriting the data of the this snapshot. + #[inline] + pub fn with_new_path(&self, path: PathBuf, new_ref: Ref, old_ref: Option) -> Self { + Self { + data: self.data, + old: old_ref, + new: new_ref, + path, + middleware: None, + } + } + + /// Allows a middleware to be 'forced' onto a SyncbackSnapshot to override + /// the attempts to derive it. + #[inline] + pub fn middleware(mut self, middleware: Middleware) -> Self { + self.middleware = Some(middleware); + self + } + + /// Returns a map of properties for an Instance from the 'new' tree + /// with filtering done to avoid noise. This method filters out properties + /// that are not meant to be present in Instances that are represented + /// specially by a path, like `LocalScript.Source` and `StringValue.Value`. + /// + /// This method is not necessary or desired for blobs like Rbxm or non-path + /// middlewares like JsonModel. + #[inline] + #[must_use] + pub fn get_path_filtered_properties(&self, new_ref: Ref) -> Option> { + let inst = self.get_new_instance(new_ref)?; + + // The only filtering we have to do is filter out properties that are + // special-cased in some capacity. + let properties = filter_properties(self.data.project, inst) + .into_iter() + .filter(|(name, _)| !filter_out_property(inst, name)) + .collect(); + + Some(properties) + } + + /// Returns a path to the provided Instance in the new DOM. This path is + /// where you would look for the object in Roblox Studio. + #[inline] + pub fn get_new_inst_path(&self, referent: Ref) -> String { + inst_path(self.new_tree(), referent) + } + + /// Returns a path to the provided Instance in the old DOM. This path is + /// where you would look for the object in Roblox Studio. + #[inline] + pub fn get_old_inst_path(&self, referent: Ref) -> String { + inst_path(self.old_tree(), referent) + } + + /// Returns an Instance from the old tree with the provided referent, if it + /// exists. + #[inline] + pub fn get_old_instance(&self, referent: Ref) -> Option> { + self.data.old_tree.get_instance(referent) + } + + /// Returns an Instance from the new tree with the provided referent, if it + /// exists. + #[inline] + pub fn get_new_instance(&self, referent: Ref) -> Option<&'sync Instance> { + self.data.new_tree.get_by_ref(referent) + } + + /// The 'old' Instance this snapshot is for, if it exists. + #[inline] + pub fn old_inst(&self) -> Option> { + self.old + .and_then(|old| self.data.old_tree.get_instance(old)) + } + + /// The 'new' Instance this snapshot is for. + #[inline] + pub fn new_inst(&self) -> &'sync Instance { + self.data + .new_tree + .get_by_ref(self.new) + .expect("SyncbackSnapshot should not contain invalid referents") + } + + /// Returns the root Project that was used to make this snapshot. + #[inline] + pub fn project(&self) -> &'sync Project { + self.data.project + } + + /// Returns the underlying VFS being used for syncback. + #[inline] + pub fn vfs(&self) -> &'sync Vfs { + self.data.vfs + } + + /// Returns the WeakDom used for the 'new' tree. + #[inline] + pub fn new_tree(&self) -> &'sync WeakDom { + self.data.new_tree + } + + /// Returns the WeakDom used for the 'old' tree. + #[inline] + pub fn old_tree(&self) -> &'sync WeakDom { + self.data.old_tree.inner() + } + + /// Returns user-specified property ignore rules. + #[inline] + pub fn ignore_props(&self) -> Option<&IndexMap>> { + self.data + .project + .syncback_rules + .as_ref() + .map(|rules| &rules.ignore_properties) + } + + /// Returns user-specified ignore tree. + #[inline] + pub fn ignore_tree(&self) -> Option<&[String]> { + self.data + .project + .syncback_rules + .as_ref() + .map(|rules| rules.ignore_trees.as_slice()) + } +} + +pub fn filter_out_property(inst: &Instance, prop_name: &str) -> bool { + match inst.class.as_str() { + "Script" | "LocalScript" | "ModuleScript" => { + // These properties shouldn't be set by scripts that are created via + // `$path` or via being on the file system. + prop_name == "Source" || prop_name == "ScriptGuid" + } + "LocalizationTable" => prop_name == "Contents", + "StringValue" => prop_name == "Value", + _ => false, + } +} + +pub fn inst_path(dom: &WeakDom, referent: Ref) -> String { + let mut path = Vec::new(); + + let mut inst = dom.get_by_ref(referent); + while let Some(instance) = inst { + path.push(instance.name.as_str()); + inst = dom.get_by_ref(instance.parent()); + } + // This is to avoid the root's name from appearing in the path. Not + // optimal, but should be fine. + path.pop(); + + path.reverse(); + path.join("/") +} + +#[cfg(test)] +mod test { + use rbx_dom_weak::{InstanceBuilder, WeakDom}; + + use super::inst_path as inst_path_outer; + + #[test] + fn inst_path() { + let mut new_tree = WeakDom::new(InstanceBuilder::new("ROOT")); + + let child_1 = new_tree.insert(new_tree.root_ref(), InstanceBuilder::new("Child1")); + let child_2 = new_tree.insert(child_1, InstanceBuilder::new("Child2")); + let child_3 = new_tree.insert(child_2, InstanceBuilder::new("Child3")); + + assert_eq!(inst_path_outer(&new_tree, new_tree.root_ref()), ""); + assert_eq!(inst_path_outer(&new_tree, child_1), "Child1"); + assert_eq!(inst_path_outer(&new_tree, child_2), "Child1/Child2"); + assert_eq!(inst_path_outer(&new_tree, child_3), "Child1/Child2/Child3"); + } +} diff --git a/src/variant_eq.rs b/src/variant_eq.rs new file mode 100644 index 00000000..0a4c058d --- /dev/null +++ b/src/variant_eq.rs @@ -0,0 +1,191 @@ +use rbx_dom_weak::types::{PhysicalProperties, Variant, Vector3}; + +/// Accepts three argumets: a float type and two values to compare. +/// +/// Returns a bool indicating whether they're equal. This accounts for NaN such +/// that `approx_eq!(f32, f32::NAN, f32::NAN)` is `true`. +macro_rules! approx_eq { + ($Ty:ty, $a:expr, $b:expr) => { + float_cmp::approx_eq!($Ty, $a, $b) || $a.is_nan() && $b.is_nan() + }; +} + +/// Compares two variants to determine if they're equal. This correctly takes +/// float comparisons into account. +pub fn variant_eq(variant_a: &Variant, variant_b: &Variant) -> bool { + if variant_a.ty() != variant_b.ty() { + return false; + } + + match (variant_a, variant_b) { + (Variant::Attributes(a), Variant::Attributes(b)) => { + // If they're not the same size, we can just abort + if a.len() != b.len() { + return false; + } + + // Since Attributes are stored with a BTreeMap, the keys are sorted + // and we can compare each map's keys in order. + for ((a_name, a_value), (b_name, b_value)) in a.iter().zip(b.iter()) { + if !(a_name == b_name && variant_eq(a_value, b_value)) { + return false; + } + } + + true + } + (Variant::Axes(a), Variant::Axes(b)) => a == b, + (Variant::BinaryString(a), Variant::BinaryString(b)) => a == b, + (Variant::Bool(a), Variant::Bool(b)) => a == b, + (Variant::BrickColor(a), Variant::BrickColor(b)) => a == b, + (Variant::CFrame(a), Variant::CFrame(b)) => { + vector_eq(&a.position, &b.position) + && vector_eq(&a.orientation.x, &b.orientation.x) + && vector_eq(&a.orientation.y, &b.orientation.y) + && vector_eq(&a.orientation.z, &b.orientation.z) + } + (Variant::Color3(a), Variant::Color3(b)) => { + approx_eq!(f32, a.r, b.r) && approx_eq!(f32, a.g, b.g) && approx_eq!(f32, a.b, b.b) + } + (Variant::Color3uint8(a), Variant::Color3uint8(b)) => a == b, + (Variant::ColorSequence(a), Variant::ColorSequence(b)) => { + if a.keypoints.len() != b.keypoints.len() { + return false; + } + let mut a_keypoints: Vec<_> = a.keypoints.iter().collect(); + let mut b_keypoints: Vec<_> = b.keypoints.iter().collect(); + a_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + b_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + + for (a_kp, b_kp) in a_keypoints.iter().zip(b_keypoints) { + if !(approx_eq!(f32, a_kp.time, b_kp.time) + && approx_eq!(f32, a_kp.color.r, b_kp.color.r) + && approx_eq!(f32, a_kp.color.g, b_kp.color.g) + && approx_eq!(f32, a_kp.color.b, b_kp.color.b)) + { + return false; + } + } + true + } + (Variant::Content(a), Variant::Content(b)) => a == b, + (Variant::ContentId(a), Variant::ContentId(b)) => a == b, + (Variant::Enum(a), Variant::Enum(b)) => a == b, + (Variant::Faces(a), Variant::Faces(b)) => a == b, + (Variant::Float32(a), Variant::Float32(b)) => approx_eq!(f32, *a, *b), + (Variant::Float64(a), Variant::Float64(b)) => approx_eq!(f64, *a, *b), + (Variant::Font(a), Variant::Font(b)) => { + a.weight == b.weight + && a.style == b.style + && a.family == b.family + && a.cached_face_id == b.cached_face_id + } + (Variant::Int32(a), Variant::Int32(b)) => a == b, + (Variant::Int64(a), Variant::Int64(b)) => a == b, + (Variant::MaterialColors(a), Variant::MaterialColors(b)) => a.encode() == b.encode(), + (Variant::NetAssetRef(a), Variant::NetAssetRef(b)) => a == b, + (Variant::NumberRange(a), Variant::NumberRange(b)) => { + approx_eq!(f32, a.max, b.max) && approx_eq!(f32, a.min, b.min) + } + (Variant::NumberSequence(a), Variant::NumberSequence(b)) => { + if a.keypoints.len() != b.keypoints.len() { + return false; + } + let mut a_keypoints: Vec<_> = a.keypoints.iter().collect(); + let mut b_keypoints: Vec<_> = b.keypoints.iter().collect(); + a_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + b_keypoints.sort_unstable_by(|k1, k2| k1.time.partial_cmp(&k2.time).unwrap()); + + for (a_kp, b_kp) in a_keypoints.iter().zip(b_keypoints) { + if !(approx_eq!(f32, a_kp.time, b_kp.time) + && approx_eq!(f32, a_kp.value, b_kp.value) + && approx_eq!(f32, a_kp.envelope, b_kp.envelope)) + { + return false; + } + } + true + } + (Variant::OptionalCFrame(a), Variant::OptionalCFrame(b)) => match (a, b) { + (Some(a), Some(b)) => { + vector_eq(&a.position, &b.position) + && vector_eq(&a.orientation.x, &b.orientation.x) + && vector_eq(&a.orientation.y, &b.orientation.y) + && vector_eq(&a.orientation.z, &b.orientation.z) + } + (None, None) => true, + _ => false, + }, + (Variant::PhysicalProperties(a), Variant::PhysicalProperties(b)) => match (a, b) { + (PhysicalProperties::Default, PhysicalProperties::Default) => true, + (PhysicalProperties::Custom(a2), PhysicalProperties::Custom(b2)) => { + approx_eq!(f32, a2.density(), b2.density()) + && approx_eq!(f32, a2.elasticity(), b2.elasticity()) + && approx_eq!(f32, a2.friction(), b2.friction()) + && approx_eq!(f32, a2.elasticity_weight(), b2.elasticity_weight()) + && approx_eq!(f32, a2.friction_weight(), b2.friction_weight()) + && approx_eq!(f32, a2.acoustic_absorption(), b2.acoustic_absorption()) + } + _ => false, + }, + (Variant::Ray(a), Variant::Ray(b)) => { + vector_eq(&a.direction, &b.direction) && vector_eq(&a.origin, &b.origin) + } + (Variant::Rect(a), Variant::Rect(b)) => { + approx_eq!(f32, a.max.x, b.max.x) + && approx_eq!(f32, a.max.y, b.max.y) + && approx_eq!(f32, a.min.x, b.min.x) + && approx_eq!(f32, a.min.y, b.min.y) + } + (Variant::Ref(a), Variant::Ref(b)) => a == b, + (Variant::Region3(a), Variant::Region3(b)) => { + vector_eq(&a.max, &b.max) && vector_eq(&a.min, &b.min) + } + (Variant::Region3int16(a), Variant::Region3int16(b)) => a == b, + (Variant::SecurityCapabilities(a), Variant::SecurityCapabilities(b)) => a == b, + (Variant::SharedString(a), Variant::SharedString(b)) => a == b, + (Variant::Tags(a), Variant::Tags(b)) => { + let mut a_sorted: Vec<&str> = a.iter().collect(); + let mut b_sorted: Vec<&str> = b.iter().collect(); + if a_sorted.len() == b_sorted.len() { + a_sorted.sort_unstable(); + b_sorted.sort_unstable(); + for (a_tag, b_tag) in a_sorted.into_iter().zip(b_sorted) { + if a_tag != b_tag { + return false; + } + } + true + } else { + false + } + } + (Variant::UDim(a), Variant::UDim(b)) => { + approx_eq!(f32, a.scale, b.scale) && a.offset == b.offset + } + (Variant::UDim2(a), Variant::UDim2(b)) => { + approx_eq!(f32, a.x.scale, b.x.scale) + && a.x.offset == b.x.offset + && approx_eq!(f32, a.y.scale, b.y.scale) + && a.y.offset == b.y.offset + } + (Variant::UniqueId(a), Variant::UniqueId(b)) => a == b, + (Variant::String(a), Variant::String(b)) => a == b, + (Variant::Vector2(a), Variant::Vector2(b)) => { + approx_eq!(f32, a.x, b.x) && approx_eq!(f32, a.y, b.y) + } + (Variant::Vector2int16(a), Variant::Vector2int16(b)) => a == b, + (Variant::Vector3(a), Variant::Vector3(b)) => vector_eq(a, b), + (Variant::Vector3int16(a), Variant::Vector3int16(b)) => a == b, + (a, b) => panic!( + "unsupport variant comparison: {:?} and {:?}", + a.ty(), + b.ty() + ), + } +} + +#[inline(always)] +fn vector_eq(a: &Vector3, b: &Vector3) -> bool { + approx_eq!(f32, a.x, b.x) && approx_eq!(f32, a.y, b.y) && approx_eq!(f32, a.z, b.z) +} diff --git a/src/web/ui.rs b/src/web/ui.rs index da620966..4d63eaad 100644 --- a/src/web/ui.rs +++ b/src/web/ui.rs @@ -165,6 +165,7 @@ impl UiService {

"specified_id: " { format!("{:?}", metadata.specified_id) }
"ignore_unknown_instances: " { metadata.ignore_unknown_instances.to_string() }
"instigating source: " { format!("{:?}", metadata.instigating_source) }
+
"middleware: " { format!("{:?}", metadata.middleware) }
{ relevant_paths } }; diff --git a/tests/rojo_test/io_util.rs b/tests/rojo_test/io_util.rs index ce940f4a..c18e457d 100644 --- a/tests/rojo_test/io_util.rs +++ b/tests/rojo_test/io_util.rs @@ -10,6 +10,8 @@ use walkdir::WalkDir; pub static ROJO_PATH: &str = env!("CARGO_BIN_EXE_rojo"); pub static BUILD_TESTS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/build-tests"); pub static SERVE_TESTS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/serve-tests"); +pub static SYNCBACK_TESTS_PATH: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/syncback-tests"); pub fn get_working_dir_path() -> PathBuf { let mut manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/tests/rojo_test/mod.rs b/tests/rojo_test/mod.rs index fd0cda3a..6ce465b4 100644 --- a/tests/rojo_test/mod.rs +++ b/tests/rojo_test/mod.rs @@ -1,3 +1,4 @@ pub mod internable; pub mod io_util; pub mod serve_util; +pub mod syncback_util; diff --git a/tests/rojo_test/syncback_util.rs b/tests/rojo_test/syncback_util.rs new file mode 100644 index 00000000..b4b54953 --- /dev/null +++ b/tests/rojo_test/syncback_util.rs @@ -0,0 +1,116 @@ +use std::{io::Write as _, path::Path, process::Command}; + +use insta::{assert_snapshot, assert_yaml_snapshot}; +use tempfile::tempdir; + +use crate::rojo_test::io_util::SYNCBACK_TESTS_PATH; + +use super::io_util::{copy_recursive, ROJO_PATH}; + +const INPUT_FILE_PROJECT: &str = "input-project"; +const INPUT_FILE_PLACE: &str = "input.rbxl"; +const INPUT_FILE_MODEL: &str = "input.rbxm"; + +/// Convenience method to run a `rojo syncback` test. +/// +/// Test projects should be defined in the `syncback-tests` folder; their filename +/// should be given as the first parameter. +/// +/// The passed in callback is where the actual test body should go. Setup and +/// cleanup happens automatically. +pub fn run_syncback_test(name: &str, callback: impl FnOnce(&Path)) { + let _ = env_logger::try_init(); + + // let working_dir = get_working_dir_path(); + + let source_path = Path::new(SYNCBACK_TESTS_PATH) + .join(name) + .join(INPUT_FILE_PROJECT); + // We want to support both rbxls and rbxms as input + let input_file = { + let mut path = Path::new(SYNCBACK_TESTS_PATH) + .join(name) + .join(INPUT_FILE_PLACE); + if !path.exists() { + path.set_file_name(INPUT_FILE_MODEL); + } + path + }; + + let test_dir = tempdir().expect("Couldn't create temporary directory"); + let project_path = test_dir + .path() + .canonicalize() + .expect("Couldn't canonicalize temporary directory path") + .join(name); + + let source_is_file = fs_err::metadata(&source_path).unwrap().is_file(); + + if source_is_file { + fs_err::copy(&source_path, &project_path).expect("couldn't copy project file"); + } else { + fs_err::create_dir(&project_path).expect("Couldn't create temporary project subdirectory"); + + copy_recursive(&source_path, &project_path) + .expect("Couldn't copy project to temporary directory"); + }; + + let output = Command::new(ROJO_PATH) + // I don't really understand why setting the working directory breaks this, but it does. + // It's a bit concerning but I'm more interested in writing tests than debugging it right now. + // TODO: Figure out why and fix it. + // .current_dir(working_dir) + .args([ + "--color", + "never", + "syncback", + project_path.to_str().unwrap(), + "--input", + input_file.to_str().unwrap(), + "--non-interactive", + "--list", + ]) + .output() + .expect("Couldn't spawn syncback process"); + + if !output.status.success() { + let mut lock = std::io::stderr().lock(); + writeln!( + lock, + "Rojo exited with status code {:?}", + output.status.code() + ) + .unwrap(); + writeln!(lock, "Stdout from process:").unwrap(); + lock.write_all(&output.stdout).unwrap(); + writeln!(lock, "Stderr from process:").unwrap(); + lock.write_all(&output.stderr).unwrap(); + + std::process::exit(1) + } + + let mut settings = insta::Settings::new(); + let snapshot_path = Path::new(SYNCBACK_TESTS_PATH) + .parent() + .unwrap() + .join("syncback-test-snapshots"); + settings.set_snapshot_path(snapshot_path); + settings.set_sort_maps(true); + + settings.bind(|| { + assert_snapshot!( + format!("{name}-stdout"), + String::from_utf8_lossy(&output.stdout) + ) + }); + + settings.bind(|| callback(project_path.as_path())) +} + +pub fn snapshot_rbxm(name: &str, input: Vec, file_name: &str) { + assert_yaml_snapshot!( + name, + rbx_binary::text_format::DecodedModel::from_reader(input.as_slice()), + file_name + ) +} diff --git a/tests/tests/mod.rs b/tests/tests/mod.rs index a348e228..bc710016 100644 --- a/tests/tests/mod.rs +++ b/tests/tests/mod.rs @@ -1,2 +1,3 @@ mod build; mod serve; +mod syncback; diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs new file mode 100644 index 00000000..41794eb9 --- /dev/null +++ b/tests/tests/syncback.rs @@ -0,0 +1,81 @@ +use std::ffi::OsStr; + +use insta::assert_snapshot; + +use crate::rojo_test::syncback_util::{run_syncback_test, snapshot_rbxm}; + +macro_rules! syncback_tests { + ($($test_name:ident => $list:expr$(,)?),*) => {$( + #[test] + fn $test_name() { + run_syncback_test(stringify!($test_name), |path| { + for name in $list { + let snapshot_name = format!(concat!(stringify!($test_name), "-{}"), name); + let new = path.join::<&str>(name); + if let Some("rbxm") = new.extension().and_then(OsStr::to_str) { + let content = fs_err::read(new).unwrap(); + snapshot_rbxm(&snapshot_name, content, name); + } else { + let content = fs_err::read_to_string(new).unwrap(); + assert_snapshot!(snapshot_name, content, name); + } + } + }); + } + )*}; +} + +syncback_tests! { + // Ensures that there's only one copy written to disk if navigating a + // project file might yield two copies + child_but_not => ["OnlyOneCopy/child_of_one.luau", "ReplicatedStorage/child_replicated_storage.luau"], + // Ensures that syncback works with CSVs + csv => ["src/csv_init/init.csv", "src/csv.csv"], + // Ensures that if a RojoId is duplicated somewhere in the project, it's + // rewritten rather than synced back as a conflict + duplicate_rojo_id => ["container.model.json"], + // Ensures that the `ignorePaths` setting works for additions + ignore_paths_adding => ["src/int_value.model.json", "src/subfolder/string_value.txt"], + // Ensures that the `ignorePaths` setting works for `init` files + ignore_paths_init => ["src/non-init.luau", "src/init-file/init.luau"], + // Ensures that the `ignorePaths` setting works for removals + ignore_paths_removing => ["src/Message.rbxm"], + // Ensures that `ignoreTrees` works for additions + ignore_trees_adding => [], + // Ensures that `ignoreTrees` works for removals + ignore_trees_removing => [], + // Ensures that all of the JSON middlewares are handled as expected + json_middlewares => ["src/dir_with_meta/init.meta.json", "src/model_json.model.json", "src/project_json.project.json"], + // Ensures projects that refer to other projects work as expected + nested_projects => ["nested.project.json", "string_value.txt"], + // Ensures files that are ignored by nested projects are picked up if + // they're included in second project. Unusual but perfectly workable + // pattern that syncback has to support. + nested_projects_weird => ["src/modules/ClientModule.luau", "src/modules/ServerModule.luau"], + // Ensures that projects respect `init` files when they're directly referenced from a node + project_init => ["src/init.luau"], + // Ensures that projects can be reserialized by syncback and that + // default.project.json doesn't change unexpectedly. + project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json"], + // Confirms that Instances that cannot serialize as directories serialize as rbxms + rbxm_fallback => ["src/ChildWithDuplicates.rbxm"], + // Ensures that ref properties are linked properly on the file system + ref_properties => ["src/pointer.model.json", "src/target.model.json"], + // Ensures that ref properties are linked when no attributes are manually + // set in the DataModel + ref_properties_blank => ["src/pointer.model.json", "src/target.meta.json", "src/target.txt"], + // Ensures that if there is a conflict in RojoRefs, one of them is rewritten. + ref_properties_conflict => ["src/Pointer_2.model.json", "src/Target_2.model.json"], + // Ensures that having multiple pointers that are aimed at the same target doesn't trigger ref rewrites. + ref_properties_duplicate => [], + // Ensures that the old middleware is respected during syncback + respect_old_middleware => ["default.project.json", "src/model_json.model.json", "src/rbxm.rbxm", "src/rbxmx.rbxmx"], + // Ensures that StringValues inside project files are written to the + // project file, but only if they don't have `$path` set + string_value_project => ["default.project.json"], + // Ensures that sync rules are respected. This is really just a test to + // ensure it uses the old path when possible, but we want the coverage. + sync_rules => ["src/module.modulescript", "src/text.text"], + // Ensures that the `syncUnscriptable` setting works + unscriptable_properties => ["default.project.json"], +}