mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
Update dependencies and implement git-based sync direction defaults
- Updated `clap` and `unicode-ident` versions in `Cargo.lock` and `Cargo.toml`. - Added `sha1` crate to `Cargo.toml` for SHA1 hashing functionality. - Introduced a new `git.rs` module for handling git operations, including changed files and SHA1 computation. - Enhanced the sync confirmation dialog to auto-select defaults based on git status, improving user experience during two-way sync. - Updated `PatchTree` to utilize git metadata for determining default selections during patch application. These changes enhance the integration with git, providing smarter defaults and improving the overall sync process.
This commit is contained in:
parent
df22500102
commit
7ba04f7f6a
29 changed files with 1285 additions and 176 deletions
|
|
@ -188,7 +188,7 @@ function ApiContext:read(ids)
|
|||
end)
|
||||
end
|
||||
|
||||
function ApiContext:write(patch)
|
||||
function ApiContext:write(patch, stageIds)
|
||||
local url = ("%s/api/write"):format(self.__baseUrl)
|
||||
|
||||
-- Log what we're sending to the server (syncback/pull)
|
||||
|
|
@ -248,6 +248,7 @@ function ApiContext:write(patch)
|
|||
removed = patch.removed,
|
||||
updated = updated,
|
||||
added = added,
|
||||
stageIds = stageIds,
|
||||
}
|
||||
|
||||
body = Http.msgpackEncode(body)
|
||||
|
|
|
|||
|
|
@ -653,10 +653,12 @@ function App:startSession()
|
|||
})
|
||||
end)
|
||||
|
||||
local cachedServerInfo = nil
|
||||
|
||||
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
|
||||
-- Build new tree for patch
|
||||
local gitMetadata = cachedServerInfo and cachedServerInfo.gitMetadata
|
||||
self:setState({
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }, gitMetadata),
|
||||
})
|
||||
end)
|
||||
self.cleanupPostcommit = serveSession:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
||||
|
|
@ -790,6 +792,8 @@ function App:startSession()
|
|||
local initialSyncConfirmed = false
|
||||
|
||||
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
||||
cachedServerInfo = serverInfo
|
||||
|
||||
-- Filter out the DataModel name change from the patch
|
||||
-- The project name (DataModel.Name) is managed by Studio independently
|
||||
PatchSet.removeDataModelName(patch, instanceMap)
|
||||
|
|
@ -864,7 +868,7 @@ function App:startSession()
|
|||
})
|
||||
self:setState({
|
||||
appStatus = AppStatus.Confirming,
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }),
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, serverInfo.gitMetadata),
|
||||
confirmData = {
|
||||
serverInfo = serverInfo,
|
||||
},
|
||||
|
|
@ -914,8 +918,9 @@ function App:startSession()
|
|||
-- Update the patchTree when new changes arrive during confirmation
|
||||
-- The changedIds parameter contains IDs of items that were modified
|
||||
-- so their selections can be reset (user must re-review)
|
||||
local gitMetadata = cachedServerInfo and cachedServerInfo.gitMetadata
|
||||
self:setState({
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }),
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }, gitMetadata),
|
||||
changedIds = changedIds, -- Pass to UI so it can unselect changed items
|
||||
})
|
||||
end)
|
||||
|
|
@ -1045,7 +1050,19 @@ function App:render()
|
|||
self.confirmationBindable:Fire("Abort")
|
||||
end,
|
||||
onConfirm = function(selections)
|
||||
self.confirmationBindable:Fire({ type = "Confirm", selections = selections })
|
||||
local autoSelectedIds = {}
|
||||
if self.state.patchTree then
|
||||
self.state.patchTree:forEach(function(node)
|
||||
if node.patchType and node.defaultSelection ~= nil then
|
||||
autoSelectedIds[node.id] = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
self.confirmationBindable:Fire({
|
||||
type = "Confirm",
|
||||
selections = selections,
|
||||
autoSelectedIds = autoSelectedIds,
|
||||
})
|
||||
end,
|
||||
}),
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ local Log = require(Packages.Log)
|
|||
|
||||
local ChangeMetadata = require(Plugin.ChangeMetadata)
|
||||
local Config = require(Plugin.Config)
|
||||
local SHA1 = require(Plugin.SHA1)
|
||||
local Timer = require(Plugin.Timer)
|
||||
local Types = require(Plugin.Types)
|
||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||
|
|
@ -212,7 +213,8 @@ local PatchTree = {}
|
|||
|
||||
-- Builds a new tree from a patch and instanceMap
|
||||
-- uses changeListHeaders in node.changeList
|
||||
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
-- gitMetadata is optional: { changedIds: {string}, scriptCommittedHashes: {[string]: {string}} }
|
||||
function PatchTree.build(patch, instanceMap, changeListHeaders, gitMetadata)
|
||||
Timer.start("PatchTree.build")
|
||||
local clock = os.clock()
|
||||
|
||||
|
|
@ -220,6 +222,14 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
|
||||
local knownAncestors = {}
|
||||
|
||||
local changedIdSet = nil
|
||||
if gitMetadata and gitMetadata.changedIds then
|
||||
changedIdSet = {}
|
||||
for _, id in gitMetadata.changedIds do
|
||||
changedIdSet[id] = true
|
||||
end
|
||||
end
|
||||
|
||||
Timer.start("patch.updated")
|
||||
for _, change in patch.updated do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
|
@ -304,8 +314,28 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
table.insert(changeList, 1, changeListHeaders)
|
||||
end
|
||||
|
||||
-- Add this node to tree
|
||||
-- Default to nil (unselected) - user must explicitly choose Push/Pull/Skip
|
||||
-- Compute default selection from git metadata
|
||||
local defaultSelection = nil
|
||||
if changedIdSet then
|
||||
if not changedIdSet[change.id] then
|
||||
defaultSelection = "pull"
|
||||
elseif instance:IsA("LuaSourceContainer")
|
||||
and gitMetadata.scriptCommittedHashes then
|
||||
local hashes = gitMetadata.scriptCommittedHashes[change.id]
|
||||
if hashes then
|
||||
local source = instance.Source
|
||||
local gitBlob = "blob " .. tostring(#source) .. "\0" .. source
|
||||
local studioHash = SHA1(buffer.fromstring(gitBlob))
|
||||
for _, hash in hashes do
|
||||
if studioHash == hash then
|
||||
defaultSelection = "push"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tree:addNode(instanceMap.fromInstances[instance.Parent], {
|
||||
id = change.id,
|
||||
patchType = "Edit",
|
||||
|
|
@ -314,7 +344,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
|||
instance = instance,
|
||||
changeInfo = changeInfo,
|
||||
changeList = changeList,
|
||||
defaultSelection = nil,
|
||||
defaultSelection = defaultSelection,
|
||||
})
|
||||
end
|
||||
Timer.stop()
|
||||
|
|
|
|||
107
plugin/src/SHA1.luau
Normal file
107
plugin/src/SHA1.luau
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
--!strict
|
||||
--!optimize 2
|
||||
--!native
|
||||
|
||||
local OFFSETS = buffer.create(320)
|
||||
|
||||
local function PreProcess(Contents: buffer): (buffer, number)
|
||||
local ContentLength = buffer.len(Contents)
|
||||
local Padding = (64 - ((ContentLength + 9) % 64)) % 64
|
||||
|
||||
local NewContentLength = ContentLength + 1 + Padding + 8
|
||||
local NewContent = buffer.create(NewContentLength)
|
||||
buffer.copy(NewContent, 0, Contents)
|
||||
buffer.writeu8(NewContent, ContentLength, 128)
|
||||
|
||||
local Length8 = ContentLength * 8
|
||||
for Index = 7, 0, -1 do
|
||||
local Remainder = Length8 % 256
|
||||
buffer.writeu8(NewContent, Index + ContentLength + 1 + Padding, Remainder)
|
||||
Length8 = (Length8 - Remainder) / 256
|
||||
end
|
||||
|
||||
return NewContent, NewContentLength
|
||||
end
|
||||
|
||||
local function DigestBlocks(Blocks: buffer, Length: number): (number, number, number, number, number)
|
||||
local A, B, C, D, E = 0x67452301, 0xefcdaB89, 0x98badcfe, 0x10325476, 0xc3d2e1f0
|
||||
local Offsets = OFFSETS
|
||||
|
||||
for Offset = 0, Length - 1, 64 do
|
||||
for BlockIndex = 0, 60, 4 do
|
||||
buffer.writeu32(Offsets, BlockIndex, bit32.byteswap(buffer.readu32(Blocks, Offset + BlockIndex)))
|
||||
end
|
||||
|
||||
for Index = 64, 316, 4 do
|
||||
buffer.writeu32(Offsets, Index, bit32.lrotate(bit32.bxor(
|
||||
buffer.readu32(Offsets, Index - 12),
|
||||
buffer.readu32(Offsets, Index - 32),
|
||||
buffer.readu32(Offsets, Index - 56),
|
||||
buffer.readu32(Offsets, Index - 64)
|
||||
), 1))
|
||||
end
|
||||
|
||||
local H1, H2, H3, H4, H5 = A, B, C, D, E
|
||||
|
||||
local Temp
|
||||
for Round = 0, 19 do
|
||||
Temp = bit32.lrotate(H1, 5) +
|
||||
bit32.band(H2, H3) + bit32.band(-1 - H2, H4) +
|
||||
H5 + 0x5a827999 +
|
||||
buffer.readu32(Offsets, Round * 4)
|
||||
|
||||
H5, H4, H3, H2, H1 = H4, H3, bit32.lrotate(H2, 30), H1, Temp
|
||||
end
|
||||
|
||||
for Round = 20, 39 do
|
||||
Temp = bit32.lrotate(H1, 5) +
|
||||
bit32.bxor(H2, H3, H4) +
|
||||
H5 + 0x6ed9eba1 +
|
||||
buffer.readu32(Offsets, Round * 4)
|
||||
|
||||
H5, H4, H3, H2, H1 = H4, H3, bit32.lrotate(H2, 30), H1, Temp
|
||||
end
|
||||
|
||||
for Round = 40, 59 do
|
||||
Temp = bit32.lrotate(H1, 5) +
|
||||
bit32.band(H4, H3) + bit32.band(H2, bit32.bxor(H4, H3)) +
|
||||
H5 + 0x8f1bbcdc +
|
||||
buffer.readu32(Offsets, Round * 4)
|
||||
|
||||
H5, H4, H3, H2, H1 = H4, H3, bit32.lrotate(H2, 30), H1, Temp
|
||||
end
|
||||
|
||||
for Round = 60, 79 do
|
||||
Temp = bit32.lrotate(H1, 5) +
|
||||
bit32.bxor(H2, H3, H4) +
|
||||
H5 + 0xca62c1d6 +
|
||||
buffer.readu32(Offsets, Round * 4)
|
||||
|
||||
H5, H4, H3, H2, H1 = H4, H3, bit32.lrotate(H2, 30), H1, Temp
|
||||
end
|
||||
|
||||
A = bit32.bor(A + H1, 0)
|
||||
B = bit32.bor(B + H2, 0)
|
||||
C = bit32.bor(C + H3, 0)
|
||||
D = bit32.bor(D + H4, 0)
|
||||
E = bit32.bor(E + H5, 0)
|
||||
end
|
||||
|
||||
return A, B, C, D, E
|
||||
end
|
||||
|
||||
local function SHA1(Message: buffer, Salt: buffer?): string
|
||||
if Salt and buffer.len(Salt) > 0 then
|
||||
local MessageWithSalt = buffer.create(buffer.len(Message) + buffer.len(Salt))
|
||||
|
||||
buffer.copy(MessageWithSalt, 0, Message)
|
||||
buffer.copy(MessageWithSalt, buffer.len(Message), Salt)
|
||||
|
||||
Message = MessageWithSalt
|
||||
end
|
||||
|
||||
local ProcessedMessage, Length = PreProcess(Message)
|
||||
return string.format("%08x%08x%08x%08x%08x", DigestBlocks(ProcessedMessage, Length))
|
||||
end
|
||||
|
||||
return SHA1
|
||||
|
|
@ -695,6 +695,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
elseif type(userDecision) == "table" and userDecision.type == "Confirm" then
|
||||
-- New: Apply based on per-item selections
|
||||
local selections = userDecision.selections or {}
|
||||
local autoSelectedIds = userDecision.autoSelectedIds or {}
|
||||
|
||||
-- Log selection summary (debug level)
|
||||
local pushCount, pullCount, ignoreCount = 0, 0, 0
|
||||
|
|
@ -713,6 +714,9 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
local pushPatch = PatchSet.newEmpty() -- Items to apply to Studio
|
||||
local pullPatch = PatchSet.newEmpty() -- Items to send back to Rojo
|
||||
|
||||
-- Build stage_ids: all push-accepted IDs + auto-selected pull-accepted IDs
|
||||
local stageIds = {}
|
||||
|
||||
-- Process updated items
|
||||
for _, change in catchUpPatch.updated do
|
||||
local selection = selections[change.id]
|
||||
|
|
@ -723,6 +727,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
-- Apply Rojo changes to Studio
|
||||
Log.info("[Push] Update: {}", instancePath)
|
||||
table.insert(pushPatch.updated, change)
|
||||
table.insert(stageIds, change.id)
|
||||
elseif selection == "pull" and self.__twoWaySync then
|
||||
-- Send Studio state back to Rojo
|
||||
if instance then
|
||||
|
|
@ -749,6 +754,9 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
)
|
||||
if update then
|
||||
table.insert(pullPatch.updated, update)
|
||||
if autoSelectedIds[change.id] then
|
||||
table.insert(stageIds, change.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -778,6 +786,9 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
-- Apply Rojo removal to Studio (delete the instance)
|
||||
Log.info("[Push] Delete: {}", instancePath)
|
||||
table.insert(pushPatch.removed, idOrInstance)
|
||||
if id then
|
||||
table.insert(stageIds, id)
|
||||
end
|
||||
elseif selection == "pull" and self.__twoWaySync then
|
||||
-- Syncback: Create file in Rojo from Studio instance
|
||||
if instance and instance.Parent then
|
||||
|
|
@ -790,6 +801,9 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
local guid = HttpService:GenerateGUID(false)
|
||||
local tempRef = string.gsub(guid, "-", ""):lower()
|
||||
pullPatch.added[tempRef] = encoded
|
||||
if id and autoSelectedIds[id] then
|
||||
table.insert(stageIds, tempRef)
|
||||
end
|
||||
-- Note: We intentionally do NOT pre-insert into InstanceMap here.
|
||||
-- The VFS watcher will process the written files and assign a
|
||||
-- server-side Ref ID. Any Ref properties targeting this instance
|
||||
|
|
@ -818,6 +832,7 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
-- Apply Rojo addition to Studio
|
||||
Log.info("[Push] Add: {}.{}", parentPath, instanceName)
|
||||
pushPatch.added[id] = change
|
||||
table.insert(stageIds, id)
|
||||
elseif selection == "pull" and self.__twoWaySync then
|
||||
-- Don't add in Studio, remove from Rojo
|
||||
-- Log at info level since this is a major file deletion operation
|
||||
|
|
@ -846,19 +861,27 @@ function ServeSession:__confirmAndApplyInitialPatch(catchUpPatch, serverInfo)
|
|||
self:__applyPatch(pushPatch)
|
||||
end
|
||||
|
||||
-- Send pull items back to Rojo server
|
||||
if self.__twoWaySync and not PatchSet.isEmpty(pullPatch) then
|
||||
local addCount = 0
|
||||
for _ in pairs(pullPatch.added) do
|
||||
addCount += 1
|
||||
-- Send pull items + stage requests to Rojo server
|
||||
local hasPullChanges = self.__twoWaySync and not PatchSet.isEmpty(pullPatch)
|
||||
local hasStageIds = #stageIds > 0
|
||||
|
||||
if hasPullChanges or hasStageIds then
|
||||
if hasPullChanges then
|
||||
local addCount = 0
|
||||
for _ in pairs(pullPatch.added) do
|
||||
addCount += 1
|
||||
end
|
||||
Log.info(
|
||||
"Sending to Rojo: {} file creations, {} file deletions, {} updates",
|
||||
addCount,
|
||||
#pullPatch.removed,
|
||||
#pullPatch.updated
|
||||
)
|
||||
end
|
||||
Log.info(
|
||||
"Sending to Rojo: {} file creations, {} file deletions, {} updates",
|
||||
addCount,
|
||||
#pullPatch.removed,
|
||||
#pullPatch.updated
|
||||
)
|
||||
return self.__apiContext:write(pullPatch)
|
||||
if hasStageIds then
|
||||
Log.info("Requesting git stage for {} files", #stageIds)
|
||||
end
|
||||
return self.__apiContext:write(pullPatch, stageIds)
|
||||
end
|
||||
|
||||
return Promise.resolve()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue