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

githubUrl_Original: "https://github.com/silverbulletmd/silverbullet-libraries/blob/main/Git.md"

  1. Add some additonal functions to Official Git.md #github
  2. https://community.silverbullet.md/t/vibe-coded-sb-v2-git-plugin/3228?u=chenzhu-xie

Enhanced Git Plug

manual/periodic/event-driven sync

image

commit (manual)

image

force pull (manual)

image

force push (manual)

image

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")}

  • Adds all files in your folder to git
  • Commits them with the default "Snapshot" commit message
  • git pulls changes from the remote server
  • git pushes changes to the remote server

${widgets.commandButton("Git: Commit")}

  • Asks you for a commit message
  • Commits

Configuration

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})

Implementation

The full implementation of this integration follows.

Configuration

-- priority: 100
config.define("git", {
  type = "object",
  properties = {
    autoSync = schema.number()
  }
})

Commands

-- 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
}