author: Chenzhu-Xie name: Library/xczphysics/CONFIG/Sync/Enhanced_Git tags: meta/library pageDecoration.prefix: "🎏 "




This library adds a basic git synchronization functionality to SilverBullet. It should be considered a successor to silverbullet-git implemented in Space Lua.
The following commands are implemented:
${widgets.commandButton("Git: Sync")}
git pulls changes from the remote servergit pushes changes to the remote server${widgets.commandButton("Git: Commit")}
There is currently only a single configuration option: git.autoSync. When set, the Git: Sync command will be run every x minutes.
Example configuration:
config.set("git.autoSync", 5)
Real configuration:
-- priority: 99
config.set("git", {autoSync = 60 * 24})
The full implementation of this integration follows.
-- priority: 100
config.define("git", {
type = "object",
properties = {
autoSync = schema.number()
}
})
-- priority: 98
-- Utility functions for consistent error handling
local function executeGitCommand(command, args, operation)
-- print("=== executeGitCommand DEBUG START ===")
-- print("command:", command)
-- print("args:", table.concat(args or {}, " "))
-- print("operation:", operation)
local success, result = pcall(function()
return shell.run(command, args)
end)
-- print("pcall success:", success)
-- print("result type:", type(result))
-- if type(result) == "table" then
-- print("=== Table contents ===")
-- for k, v in pairs(result) do
-- print(" " .. tostring(k) .. ":", tostring(v))
-- end
-- print("=== End table ===")
-- else
-- print("result value:", tostring(result))
-- end]
if not success then
local errorMsg = tostring(result or "Unknown error")
-- print("pcall failed, returning:", operation .. " failed: " .. errorMsg)
return false, operation .. " failed: " .. errorMsg
end
if result == nil then
-- print("result is nil, returning error")
return false, operation .. " failed: No result returned from git command"
end
if type(result) == "table" then
local out = ""
local exitCode = result.code or 0
-- print("exitCode:", exitCode)
-- print("result.stdout:", tostring(result.stdout))
-- print("result.stderr:", tostring(result.stderr))
if result.stderr and result.stderr ~= "" then
out = result.stderr
elseif result.stdout and result.stdout ~= "" then
out = result.stdout
else
out = "Git command completed with no output"
end
-- print("final out:", out)
if exitCode ~= 0 then
-- print("returning false due to non-zero exit code")
return false, operation .. " failed: " .. out
else
-- print("returning true with output:", out)
return true, out
end
elseif type(result) == "string" then
-- print("result is string, returning:", result)
return true, result
elseif type(result) == "number" then
-- print("result is number:", result)
if result ~= 0 then
return false, operation .. " failed with exit code: " .. tostring(result)
else
return true, operation .. " completed successfully"
end
else
-- print("result is other type, returning:", tostring(result))
return true, operation .. " completed: " .. tostring(result)
end
-- print("=== executeGitCommand DEBUG END ===")
end
-- Notification manager for consistent messaging
local NotificationManager = {
messages = {
GIT_LOCK_WARNING = "Git operation '%s' is already running. Please wait.",
COMMIT_START = "Committing local changes...",
COMMIT_STEP = "Step 1/3: Committing local changes...",
PULL_STEP = "Step 2/3: Pulling from remote...",
PUSH_STEP = "Step 3/3: Pushing to remote...",
SYNC_SUCCESS = "Git sync successful.",
SYNC_MANUAL_START = "Starting manual Git sync...",
SYNC_MANUAL_COMPLETE = "Manual Git sync complete!",
NOTHING_TO_COMMIT = "No changes to commit",
FORCE_PUSH_START = "Starting force push process...",
FORCE_PULL_START = "Starting force pull process...",
AUTO_SYNC_TRIGGER = "Triggering %s Git sync...",
AUTO_SYNC_COMPLETE = "%s: Git sync complete.",
AUTO_SYNC_PULL_ONLY = "%s: Git pull complete.",
COMMIT_SUCCESS = "Commit successful!",
NOTHING_TO_COMMIT_CLEAN = "Nothing to commit: working tree is clean.",
GIT_OPERATION_IN_PROGRESS = "Another git operation is in progress"
}
}
function NotificationManager:show(messageKey, level, ...)
local template = self.messages[messageKey] or messageKey
local message = string.format(template, ...)
editor.flashNotification(message, level or "info")
end
function NotificationManager:showError(messageKey, ...)
self:show(messageKey, "error", ...)
end
function NotificationManager:showInfo(messageKey, ...)
self:show(messageKey, "info", ...)
end
function NotificationManager:showWarning(messageKey, ...)
self:show(messageKey, "warning", ...)
end
-- Simple memory lock mechanism for SB internal git operations
if not _G then
gitLock = {
isLocked = false,
currentOperation = ""
}
acquireGitLock = function(operationName)
if gitLock.isLocked then
NotificationManager:showWarning("GIT_LOCK_WARNING", gitLock.currentOperation)
return false
end
gitLock.isLocked = true
gitLock.currentOperation = operationName
return true
end
releaseGitLock = function()
gitLock.isLocked = false
gitLock.currentOperation = ""
end
else
_G.gitLock = {
isLocked = false,
currentOperation = ""
}
_G.acquireGitLock = function(operationName)
if _G.gitLock.isLocked then
NotificationManager:showWarning("GIT_LOCK_WARNING", _G.gitLock.currentOperation)
return false
end
_G.gitLock.isLocked = true
_G.gitLock.currentOperation = operationName
return true
end
_G.releaseGitLock = function()
_G.gitLock.isLocked = false
_G.gitLock.currentOperation = ""
end
-- Also create local references for consistency
gitLock = _G.gitLock
acquireGitLock = _G.acquireGitLock
releaseGitLock = _G.releaseGitLock
end
-- Git operation handlers (low-level operations, no locks)
local GitOperations = {}
function GitOperations.addFiles()
return executeGitCommand("git", {"add", "./*"}, "Git add")
end
function GitOperations.commitChanges(message)
return executeGitCommand("git", {"commit", "-a", "-m", message}, "Git commit")
end
function GitOperations.pullChanges()
return executeGitCommand("git", {"pull", "origin", "main"}, "Git pull")
end
function GitOperations.pushChanges()
return executeGitCommand("git", {"push", "origin", "main"}, "Git push")
end
function GitOperations.fetchFromRemote()
return executeGitCommand("git", {"fetch", "origin"}, "Git fetch")
end
function GitOperations.resetHard()
return executeGitCommand("git", {"reset", "--hard", "origin/main"}, "Git reset")
end
function GitOperations.cleanUntracked()
return executeGitCommand("git", {"clean", "-fd"}, "Git clean")
end
function GitOperations.getRemoteUrl()
return executeGitCommand("git", {"remote", "get-url", "origin"}, "Git get remote URL")
end
function GitOperations.removeGitDirectory()
-- Try Windows first
local winSuccess, winResult = executeGitCommand("cmd", {"/c", "rd", "/s", "/q", ".git"}, "Remove .git directory (Windows)")
if winSuccess then
return true, "Windows"
end
-- Try Unix/Linux
local linuxSuccess, linuxResult = executeGitCommand("rm", {"-rf", ".git"}, "Remove .git directory (Unix/Linux)")
if linuxSuccess then
return true, "Unix/Linux"
end
return false, "Failed to delete .git directory on both Windows and Unix/Linux systems"
end
function GitOperations.initRepository()
return executeGitCommand("git", {"init", "-b", "main"}, "Git init")
end
function GitOperations.addRemoteOrigin(remoteUrl)
return executeGitCommand("git", {"remote", "add", "origin", remoteUrl}, "Git add remote")
end
function GitOperations.forcePush()
return executeGitCommand("git", {"push", "--force", "origin", "main"}, "Git force push")
end
function GitOperations.handlePushError(errorMessage, isColdStart)
local formattedError = errorMessage:gsub("^Git push failed:", "Only push failed:")
local syncResultMessage = "Only push failed"
NotificationManager:showWarning(syncResultMessage .. ": " .. formattedError)
return true, syncResultMessage
end
-- Core commit operation WITH LOCK
function GitOperations.performCommit(message, showSteps, isColdStart)
local needToReleaseLock = false
local lockObj = _G and _G.gitLock or gitLock
local acquireFn = _G and _G.acquireGitLock or acquireGitLock
local releaseFn = _G and _G.releaseGitLock or releaseGitLock
if not lockObj.isLocked then
if not acquireFn("Git Commit") then
return false, NotificationManager.messages.GIT_OPERATION_IN_PROGRESS
end
needToReleaseLock = true
end
message = message or "Snapshot"
showSteps = showSteps == nil and true or showSteps
-- Step 1: Commit (add & commit)
local commitResult, commitMessage = GitOperations.performCommitInternal(message, showSteps, isColdStart)
if needToReleaseLock then releaseFn() end -- 🔓
return commitResult, commitMessage
end
-- Core sync operation WITH LOCK (this is a core function that needs locking)
function GitOperations.performSync(showSteps, isColdStart)
local needToReleaseLock = false
local lockObj = _G and _G.gitLock or gitLock
local acquireFn = _G and _G.acquireGitLock or acquireGitLock
local releaseFn = _G and _G.releaseGitLock or releaseGitLock
if not lockObj.isLocked then
if not acquireFn("Git Sync") then
return false, NotificationManager.messages.GIT_OPERATION_IN_PROGRESS
end
needToReleaseLock = true
end
showSteps = showSteps == nil and true or showSteps
-- Step 1: commit
local commitResult, commitMessage = GitOperations.performCommitInternal(nil, showSteps, true)
if commitResult == false then
if needToReleaseLock then releaseFn() end
return false, commitMessage
elseif commitResult == "nothing" then
NotificationManager:showWarning("NOTHING_TO_COMMIT_CLEAN")
end
-- Step 2: pull
if showSteps then NotificationManager:showInfo("PULL_STEP") end
local pullSuccess, pullMessage = GitOperations.pullChanges()
if not pullSuccess then
if needToReleaseLock then releaseFn() end
return false, pullMessage
end
-- Step 3: push
if showSteps then NotificationManager:showInfo("PUSH_STEP") end
local pushSuccess, pushMessage = GitOperations.pushChanges()
if not pushSuccess then
local handled, message = GitOperations.handlePushError(pushMessage, isColdStart)
if needToReleaseLock then releaseFn() end
return handled, message
end
if needToReleaseLock then releaseFn() end
return true, NotificationManager.messages.SYNC_SUCCESS
end
-- Internal commit function (no lock, used when lock is already acquired)
function GitOperations.performCommitInternal(message, showSteps, isColdStart)
message = message or "Snapshot"
showSteps = showSteps == nil and true or showSteps
if showSteps then
if isColdStart then
NotificationManager:showInfo("COMMIT_STEP")
else
NotificationManager:showInfo("COMMIT_START")
end
end
-- Step 1: add files
local addSuccess, addResult = GitOperations.addFiles()
if not addSuccess then
return false, addResult
end
-- Step 2: commit changes
local commitSuccess, commitResult = GitOperations.commitChanges(message)
if commitSuccess then
return true, commitResult
end
-- Step 3: nothing to commit
if commitResult:find("nothing to commit") then
return "nothing", NotificationManager.messages.NOTHING_TO_COMMIT_CLEAN
else
return false, commitResult
end
end
-- GitConfigValidator
if not GitConfigValidator then
-- Redefine GitConfigValidator here if needed
GitConfigValidator = {
expectedConfig = {
userName = "Xcz",
userEmail = "294302704@qq.com",
credentialHelper = "store",
pullRebase = "false",
pushAutoRemote = "true"
}
}
function GitConfigValidator:executeCommand(command, args, suppressOutput)
local success, result = pcall(function()
return shell.run(command, args)
end)
if not success then
return false, nil
end
if type(result) == "table" then
local output = result.stdout or ""
local exitCode = result.code or 0
if exitCode == 0 then
local function safe_trim(s)
if not s or s == "" then
return ""
end
return s:gsub("%s+$", "")
end
return true, safe_trim(output)
else
return false, result.stderr or ""
end
end
return success, result
end
function GitConfigValidator:getOriginUrlFromInitScript()
-- First, try to get the URL from existing git config using GitOperations
if GitOperations and GitOperations.getRemoteUrl then
local urlSuccess, urlResult = GitOperations.getRemoteUrl()
if urlSuccess and urlResult and urlResult ~= "" then
-- Clean up the URL (remove quotes, trailing whitespace)
local cleanUrl = urlResult:gsub("%s+", "")
if cleanUrl ~= "" then
return cleanUrl
end
end
end
-- If no remote URL found in git config, try to read from init script
local paths = {"../init.sh", "../init.bat", "../init.cmd"}
for _, path in ipairs(paths) do
local success, content = pcall(function()
local file = io.open(path, "r")
if file then
local content = file:read("*all")
file:close()
return content
end
return nil
end)
if success and content then
-- Look for GitHub URL patterns in the script
local patterns = {
"https://[^%s]+@github%.com/[^%s]+",
"git@github%.com:[^%s]+",
"https://github%.com/[^%s]+"
}
for _, pattern in ipairs(patterns) do
local url = content:match(pattern)
if url then
-- Clean up the URL (remove quotes, trailing characters)
url = url:gsub("[\"'`]", ""):gsub("[%s%c]+$", "")
return url
end
end
end
end
return nil
end
function GitConfigValidator:isGitRepository()
return self:executeCommand("git", {"rev-parse", "--git-dir"}, true)
end
function GitConfigValidator:initializeRepository()
if not self:isGitRepository() then
return self:executeCommand("git", {"init", "-b", "main"}, true)
end
return true
end
function GitConfigValidator:getConfigValue(key, scope)
local args = {"config"}
if scope == "global" then
table.insert(args, "--global")
elseif scope == "local" then
table.insert(args, "--local")
end
table.insert(args, key)
local success, result = self:executeCommand("git", args, true)
return success and result or nil
end
function GitConfigValidator:setConfigValue(key, value, scope)
local args = {"config"}
if scope == "global" then
table.insert(args, "--global")
elseif scope == "local" then
table.insert(args, "--local")
end
table.insert(args, key)
table.insert(args, value)
-- Use executeCommand which returns success based on exit code, not output
-- editor.flashNotification("1234", "warning")
local success, output = self:executeCommand("git", args, true)
-- Debug: Check what executeCommand actually returns
-- editor.flashNotification("etst", "warning")
-- print("setConfigValue debug - success:", success, "output:", output or "(nil)")
if success then
-- Construct the git command string for display
local gitCommand = "git config"
if scope == "global" then
gitCommand = gitCommand .. " --global"
elseif scope == "local" then
gitCommand = gitCommand .. " --local"
end
gitCommand = gitCommand .. " " .. key .. " " .. value
-- Flash notification and print
editor.flashNotification("Config set: " .. gitCommand, "warning")
-- print("Git config set: " .. gitCommand)
else
editor.flashNotification("Failed to set config:" .. (output or "(no error)"), "warning")
-- print("Failed to set config:", key, "=", value, "Error:", output or "(no error)")
end
return success
end
function GitConfigValidator:setRemoteOrigin(url)
-- First try to set existing origin (local)
local success = self:executeCommand("git", {"remote", "set-url", "origin", url}, true)
if success then
return true
end
-- If that fails, try to add new origin (local)
return self:executeCommand("git", {"remote", "add", "origin", url}, true)
end
function GitConfigValidator:checkAndSetConfig(key, expectedValue)
-- Check global first
local globalValue = self:getConfigValue(key, "global")
if globalValue == expectedValue then
local gitCommand = "git config" .. " --global"
gitCommand = gitCommand .. " " .. key .. " " .. value
editor.flashNotification("Already set: " .. gitCommand, "warning")
return true -- Already set in global, no need to set locally
end
-- Check local
local localValue = self:getConfigValue(key, "local")
if localValue == expectedValue then
local gitCommand = "git config" .. " --local"
gitCommand = gitCommand .. " " .. key .. " " .. value
editor.flashNotification("Already set: " .. gitCommand, "warning")
return true -- Already set locally
end
-- Neither global nor local has the expected value, set it locally
return self:setConfigValue(key, expectedValue, "local")
end
function GitConfigValidator:validateAndSetup()
-- Step 1: Ensure git repository is initialized
if not self:initializeRepository() then
return false -- Failed to initialize repository
end
-- Step 2: Check and set basic git config
local configKeys = {
["user.name"] = self.expectedConfig.userName,
["user.email"] = self.expectedConfig.userEmail,
["credential.helper"] = self.expectedConfig.credentialHelper,
["pull.rebase"] = self.expectedConfig.pullRebase,
["push.autoSetupRemote"] = self.expectedConfig.pushAutoRemote
}
for key, expectedValue in pairs(configKeys) do
local configSet = self:checkAndSetConfig(key, expectedValue)
if not configSet then
editor.flashNotification("Warning: Failed to set config " .. key .. " = " .. expectedValue, "warning")
-- print("Warning: Failed to set config " .. key .. " = " .. expectedValue)
end
end
-- Step 3: Check and set origin URL (only from init script)
local currentOriginUrl = self:getRemoteOriginUrl()
if not currentOriginUrl or currentOriginUrl == "" then
local scriptUrl = self:getOriginUrlFromInitScript()
if scriptUrl then
self:setRemoteOrigin(scriptUrl)
end
end
return true
end
end
-- Core force push initial commit WITH LOCK
function GitOperations.performForcePushInitial()
-- 🔒 ACQUIRE LOCK
local acquireFn = _G and _G.acquireGitLock or acquireGitLock
local releaseFn = _G and _G.releaseGitLock or releaseGitLock
if not acquireFn("Git Force Push Initial") then
return false, NotificationManager.messages.GIT_OPERATION_IN_PROGRESS
end
-- Step 1: Get remote URL from init script (before deleting .git)
editor.flashNotification("Step 1/6: Reading remote URL from init script...", "info")
local remoteUrl = GitConfigValidator:getOriginUrlFromInitScript()
if not remoteUrl or remoteUrl == "" then
NotificationManager:showWarning("No remote URL found in init script. Please enter the remote repository URL.")
remoteUrl = editor.prompt("Enter remote repository URL:")
if not remoteUrl or remoteUrl == "" then
releaseFn() -- 🔓
return false, "Force push cancelled: no remote URL provided."
end
end
editor.flashNotification("Remote URL: " .. remoteUrl, "warning")
-- Step 2: Remove .git directory
editor.flashNotification("Step 2/6: Wiping local history...", "info")
local deleteSuccess, osType = GitOperations.removeGitDirectory()
if not deleteSuccess then
releaseFn() -- 🔓
return false, osType
end
-- Step 3: Re-initialize repository and configure it
editor.flashNotification("Step 3/6: Re-initializing repository on 'main' branch...", "info")
-- Initialize repository
local initSuccess, initResult = GitOperations.initRepository()
if not initSuccess then
releaseFn() -- 🔓
return false, initResult
end
-- 🔥 NEW: Configure git after re-initialization
editor.flashNotification("Configuring git settings...", "warning")
local configSuccess, configErr = pcall(function()
GitConfigValidator:validateAndSetup()
end)
if not configSuccess then
releaseFn() -- 🔓 Release the lock before returning
-- return false, "Git configuration failed: " .. tostring(configErr)
end
-- Step 4: Re-link to remote
editor.flashNotification("Step 4/6: Re-linking to remote...", "info")
local remoteSuccess, remoteResult = GitOperations.addRemoteOrigin(remoteUrl)
if not remoteSuccess then
releaseFn() -- 🔓
return false, remoteResult
end
-- Step 5: Create initial commit
editor.flashNotification("Step 5/6: Creating initial commit...", "info")
local commitSuccess, commitMessage = GitOperations.performCommitInternal("Initial Commit", false, false)
if commitSuccess ~= true then
releaseFn() -- 🔓
return false, "Error during initial commit: " .. commitMessage
end
-- Step 6: Force push
editor.flashNotification("Step 6/6: Force pushing to remote...", "info")
local pushSuccess, pushResult = GitOperations.forcePush()
if not pushSuccess then
releaseFn() -- 🔓
return false, pushResult
end
releaseFn() -- 🔓
return true, "Force push initial commit successful!"
end
-- Force push (without wiping local history)
function GitOperations.performForcePushNoWipe()
-- 🔒 ACQUIRE LOCK
local acquireFn = _G and _G.acquireGitLock or acquireGitLock
local releaseFn = _G and _G.releaseGitLock or releaseGitLock
if not acquireFn("Git Force Push (No Wipe)") then
return false, NotificationManager.messages.GIT_OPERATION_IN_PROGRESS
end
-- Step 1/3: Ensure remote URL (git config → init script → prompt)
editor.flashNotification("Step 1/3: Ensuring remote 'origin' URL...", "info")
local remoteUrl = nil
-- Try existing git config
local haveOrigin, originOut = GitOperations.getRemoteUrl()
if haveOrigin and originOut and originOut ~= "" then
remoteUrl = (type(originOut) == "string") and originOut:gsub("%s+$", "") or originOut
end
-- Fallback to init script
if not remoteUrl or remoteUrl == "" then
remoteUrl = GitConfigValidator:getOriginUrlFromInitScript()
if remoteUrl and remoteUrl ~= "" then
local addOk, addMsg = GitOperations.addRemoteOrigin(remoteUrl)
if not addOk then
releaseFn() -- 🔓
return false, "Failed to set remote origin: " .. tostring(addMsg or "(unknown error)")
end
end
end
-- Prompt as last resort
if not remoteUrl or remoteUrl == "" then
NotificationManager:showWarning("No remote URL found in git config or init script. Please enter the remote repository URL.")
remoteUrl = editor.prompt("Enter remote repository URL:")
if not remoteUrl or remoteUrl == "" then
releaseFn() -- 🔓
return false, "Force push cancelled: no remote URL provided."
end
local addOk, addMsg = GitOperations.addRemoteOrigin(remoteUrl)
if not addOk then
releaseFn() -- 🔓
return false, "Failed to set remote origin: " .. tostring(addMsg or "(unknown error)")
end
end
editor.flashNotification("Remote URL: " .. remoteUrl, "warning")
-- Step 2/3: Committing local changes
editor.flashNotification("Step 2/3: Committing local changes...", "info")
-- Pass showSteps=false to avoid double-notifying; wrapper provides step messages
local commitOk, commitMsg = GitOperations.performCommitInternal(nil, false, false)
if commitOk == false then
releaseFn() -- 🔓
return false, commitMsg
elseif commitOk == "nothing" then
NotificationManager:showInfo("NOTHING_TO_COMMIT_CLEAN")
else
NotificationManager:showInfo("COMMIT_SUCCESS")
end
-- Step 3/3: Force pushing to remote
editor.flashNotification("Step 3/3: Force pushing to remote...", "info")
local pushOk, pushMsg = GitOperations.forcePush()
if not pushOk then
releaseFn() -- 🔓
return false, pushMsg
end
releaseFn() -- 🔓
return true, "Force push (no wipe) successful!"
end
-- Core force pull WITH LOCK
function GitOperations.performForcePull()
-- 🔒 ACQUIRE LOCK
local acquireFn = _G and _G.acquireGitLock or acquireGitLock
local releaseFn = _G and _G.releaseGitLock or releaseGitLock
if not acquireFn("Git Force Pull") then
return false, NotificationManager.messages.GIT_OPERATION_IN_PROGRESS
end
-- Step 1: Fetch
editor.flashNotification("Step 1/3: Fetching from remote...", "info")
local fetchSuccess, fetchResult = GitOperations.fetchFromRemote()
if not fetchSuccess then
releaseFn() -- 🔓
return false, fetchResult
end
-- Step 2: Reset hard
editor.flashNotification("Step 2/3: Resetting local to match remote...", "info")
local resetSuccess, resetResult = GitOperations.resetHard()
if not resetSuccess then
releaseFn() -- 🔓
return false, resetResult
end
-- Step 3: Clean untracked files
editor.flashNotification("Step 3/3: Cleaning untracked files...", "info")
local cleanSuccess, cleanResult = GitOperations.cleanUntracked()
if not cleanSuccess then
releaseFn() -- 🔓
return false, cleanResult
end
releaseFn() -- 🔓
return true, "Force pull successful! Local is now identical to remote."
end
-- Main git object (these core functions now handle their own locking)
git = {}
git.commit = GitOperations.performCommit
git.sync = GitOperations.performSync
-- SIMPLIFIED COMMAND DEFINITIONS (NO EXPLICIT LOCKING - core functions handle it)
command.define {
name = "Git: Commit",
run = function()
local message = editor.prompt("Commit message:")
if not message then return end
local success, result = git.commit(message, true, nil)
if success == false then
NotificationManager:showError(result)
elseif success == "nothing" then
editor.flashNotification("Nothing to commit: working tree is clean.", "warning")
else
editor.flashNotification("Commit successful!\n" .. result, "warning")
end
end
}
command.define {
name = "Git: Sync",
run = function()
editor.flashNotification("Starting manual Git sync...", "warning")
local success, message = git.sync(true, false) -- Show steps, not cold start
if success == true then
if message == "Only push failed" then
editor.flashNotification("Manual: Git pull complete. " .. message .. ".", "warning")
else
editor.flashNotification("Manual Git sync complete!", "warning")
end
elseif success == "nothing" then
editor.flashNotification("Nothing to commit: working tree is clean.", "warning")
else
NotificationManager:showError(message)
end
end
}
command.define {
name = "Git: Force Push (No Wipe)",
run = function()
editor.flashNotification("Starting force push (no wipe)...", "warning")
local success, message = GitOperations.performForcePushNoWipe()
if success then
editor.flashNotification(message, "warning")
else
NotificationManager:showError(message)
end
end
}
command.define {
name = "Git: Force Push Initial Commit",
run = function()
if not editor.confirm("DANGER: This will WIPE your entire Git history (local and remote) and start over. Are you absolutely sure?") then
return
end
editor.flashNotification("Starting force push process...", "warning")
local success, message = GitOperations.performForcePushInitial()
if success then
editor.flashNotification(message, "warning")
else
NotificationManager:showError(message)
end
end
}
command.define {
name = "Git: Force Pull to Overwrite Local",
run = function()
if not editor.confirm("DANGER: This will DISCARD all local changes and commits, making your local copy identical to the remote. Are you sure?") then
return
end
editor.flashNotification("Starting force pull process...", "warning")
local success, message = GitOperations.performForcePull()
if success then
editor.flashNotification(message, "warning")
else
NotificationManager:showError(message)
end
end
}
-- =========================================================
-- Auto-sync manager (SIMPLIFIED - no explicit locking, core functions handle it)
local AutoSyncManager = {
lastPeriodicSync = 0,
coldStartSyncTriggered = false,
startupTime = os.time(),
eventSyncScheduledAt = 0,
eventSyncDelaySeconds = 60
}
function AutoSyncManager:shouldTriggerSync()
local now = os.time()
local uptime = now - self.startupTime
local periodicSyncMinutes = config.get("git.autoSync")
-- Cold start sync
if periodicSyncMinutes and not self.coldStartSyncTriggered and uptime >= 5 and uptime <= 10 then
return "cold-start"
end
-- Periodic sync
if periodicSyncMinutes and self.coldStartSyncTriggered and (now - self.lastPeriodicSync) / 60 >= periodicSyncMinutes then
return "periodic"
end
-- Event-driven sync
if self.eventSyncScheduledAt > 0 and (now - self.eventSyncScheduledAt) >= self.eventSyncDelaySeconds then
return "event-driven"
end
return nil
end
-- AUTO SYNC FUNCTION
function AutoSyncManager:performAutoSync(syncType)
-- print("Triggering " .. syncType:lower() .. " git sync...")
editor.flashNotification("Triggering " .. syncType .. " Git sync...", "warning")
local isColdStart = (syncType == "Startup")
local syncOk, syncMessage = git.sync(true, isColdStart) -- Always show steps (true)
if syncOk == true then
if syncMessage == "Only push failed" then
editor.flashNotification(syncType .. ": Git pull complete. " .. syncMessage .. ".", "warning")
else
editor.flashNotification(syncType .. ": Git sync complete.", "warning")
end
-- print(syncType .. " sync completed successfully")
elseif syncOk == "nothing" then
editor.flashNotification(syncMessage, "warning")
-- print(syncType .. " sync: nothing to commit")
elseif syncOk == false then
if syncMessage == NotificationManager.messages.GIT_OPERATION_IN_PROGRESS then
editor.flashNotification(syncMessage, "warning")
-- print(syncType .. " sync warning: " .. syncMessage)
else
editor.flashNotification(syncMessage, "error")
-- print(syncType .. " sync failed:\n" .. syncMessage)
end
else
editor.flashNotification("Unknown git sync result", "error")
-- print(syncType .. " sync unknown result:", syncOk, syncMessage)
end
end
function AutoSyncManager:updateSyncState(syncType)
local now = os.time()
if syncType == "cold-start" then
self.coldStartSyncTriggered = true
self.lastPeriodicSync = now
self.eventSyncScheduledAt = 0
elseif syncType == "periodic" then
self.lastPeriodicSync = now
self.eventSyncScheduledAt = 0
else -- event-driven
self.eventSyncScheduledAt = 0
self.lastPeriodicSync = now
end
end
-- Initialize auto-sync configuration
local periodicSyncMinutes = config.get("git.autoSync")
-- if periodicSyncMinutes then
-- print("Enabling periodic git auto sync every " .. periodicSyncMinutes .. " minutes")
-- end
-- FIXED EVENT LISTENERS (remove unnecessary lock checks)
event.listen {
name = "page:saved",
run = function(event)
local pageName = event.data and event.data.name or "?"
-- print("Page '" .. pageName .. "' saved. Scheduling git sync in " .. AutoSyncManager.eventSyncDelaySeconds/60 .. " minutes.")
AutoSyncManager.eventSyncScheduledAt = os.time()
end
}
-- print("Event-driven git sync enabled (on page save).")
event.listen {
name = "cron:secondPassed",
run = function()
local syncType = AutoSyncManager:shouldTriggerSync()
if syncType then
-- print("=== " .. syncType .. " sync trigger detected ===")
-- Update sync state
AutoSyncManager:updateSyncState(syncType)
-- Execute sync (locking handled internally by git.sync)
if syncType == "cold-start" then
-- print("=== Cold Start Sync Triggered ===")
-- print("Uptime:", os.time() - AutoSyncManager.startupTime, "seconds")
AutoSyncManager:performAutoSync("Startup")
elseif syncType == "periodic" then
AutoSyncManager:performAutoSync("Periodic")
else -- event-driven
AutoSyncManager:performAutoSync("Event-driven")
end
end
end
}