[SPACE-LUA] Addon with missing Git commands (history, diff, restore)

I found the problem.

not your plugin’s problem,

but SilverBullet’s problem:

virtual pages whose name contains / cannot be properly parsed on windows machine:

like the virtual page tag:meta/api from the tag #meta/api below

time to raise a github issue -_-||


[UPDATE]

ON SilverBullet 2.3.0-0-g21ddcb4, problem related to virtual pages containing / has been fixed, which means pages that contains folder structure is now able to be git: historyed.

however, git: history acts on pages that contains _ will raise problem: Path CONFIG/Paste_as/Cursor_Anchor_79320637 corrupted both on linux and windows.

here’s the fixed code:

-- ###########################################################################
-- ## Git History Module (Fixed for paths with underscores)
-- ## Depends on: Utilities.md (utilities.debug), and environment helpers:
-- ##   string.trim, string.split, string.startsWith, shell.run, editor.*, virtualPage.*, command.*
-- ###########################################################################

-- ===========================================================================
-- == Configuration
-- ===========================================================================
local LOG_ENABLE = false

local function log(...)
  if LOG_ENABLE and utilities and utilities.debug then
     if type(utilities.debug) == "function" then 
       utilities.debug(table.concat({...}, " "))
     end  
  end
end

-- ===========================================================================
-- == Shell Helpers
-- ===========================================================================
---
-- Executes a shell command and returns its stdout.
-- Throws an error on non-zero exit code.
-- @param cmd string
-- @param args table
-- @return string stdout
local function run_shell_command(cmd, args)
  log("Running command:", cmd, table.concat(args or {}, " "))
  local result = shell.run(cmd, args)
  if not result then
    error("shell.run returned nil for: " .. cmd)
  end
  if result.code ~= 0 then
    error("Command failed: " .. cmd .. " " .. table.concat(args or {}, " ") .. "\n" .. (result.stderr or ""))
  end
  return result.stdout or ""
end

-- ===========================================================================
-- == Git Status & Properties
-- ===========================================================================
---
-- Returns true if current working dir is a git repository.
local function is_git_repo()
  local result = run_shell_command("git", { "rev-parse", "--is-inside-work-tree" })
  return string.trim(result) == "true"
end

---
-- Returns true if a file is tracked by git.
-- @param file_path string
local function is_git_tracked(file_path)
  local stdout = run_shell_command("git", { "status", "--porcelain", "--", file_path })
  return not string.startsWith(string.trim(stdout), "??")
end

---
-- Returns true if a file has uncommitted changes.
-- @param file_path string
local function has_uncommitted_changes(file_path)
  local stdout = run_shell_command("git", { "status", "--porcelain", "--", file_path })
  return string.trim(stdout) ~= ""
end

-- ===========================================================================
-- == Git Log & File Access
-- ===========================================================================
---
-- Format git unix timestamp to human string.
-- @param ts number (seconds)
local function format_git_timestamp(ts)
  return os.date("%Y-%m-%d at %H:%M:%S", ts)
end

---
-- Get the newest commit hash for a file.
-- @param file_path string
local function get_newest_version(file_path)
  local stdout = run_shell_command("git", { "log", "-1", "--format=%h", "--", file_path })
  return string.trim(stdout)
end

---
-- Get the contents of a file at a specific commit.
-- @param file_path string (path relative to repo)
-- @param hash string commit hash
local function get_file_contents(file_path, hash)
  log("get_file_contents:", file_path, "at", hash)
  return run_shell_command("git", { "show", hash .. ":" .. file_path })
end

---
-- Get commit history for a given file.
-- Returns a table of entries suitable for editor.filterBox.
-- Each entry: { name, description, ref, type, prefix, timestamp }
-- @param file_path string
local function get_history(file_path)
  log("get_history:", file_path)
  local stdout = run_shell_command("git", { "log", "--format=%h %ct %s", "--", file_path })
  stdout = string.trim(stdout)
  if stdout == "" then
    return {}
  end

  local lines = string.split(stdout, "\n")
  local commits = {}

  for _, line in ipairs(lines) do
    if line and line ~= "" then
      local parts = string.split(line, " ", 3)
      if #parts == 3 then
        local hash = parts[1]
        local ts = tonumber(parts[2]) or 0
        local msg = parts[3] or ""
        table.insert(commits, {
          name        = hash,
          description = msg .. " - " .. format_git_timestamp(ts),
          -- Note: The ref format is "path_hash". We rely on get_content to parse this correctly.
          ref         = string.gsub(file_path, "%.md$", "") .. "_" .. hash,
          type        = "commits",
          prefix      = "⚡",
          timestamp   = ts
        })
      end
    end
  end

  table.sort(commits, function(a, b)
    return (a.timestamp or 0) > (b.timestamp or 0)
  end)

  return commits
end

-- ===========================================================================
-- == Git Status Renderer
-- ===========================================================================
---
---
-- Get and render git status.
local function get_git_status()
  local raw = run_shell_command("git", { "status", "--porcelain" })
  return gitstatus_render(string.trim(raw))
end

-- ===========================================================================
-- == Diff Tools
-- ===========================================================================

---
-- Compute diff between two commits for a given file path.
-- @param hash_old string
-- @param hash_new string
-- @param file_path string (path to file in repo)
local function get_diff_between_commits(hash_old, hash_new, file_path)
  log("get_diff_between_commits:", hash_old, hash_new, file_path)
  local raw = run_shell_command("git", { "diff", "--no-color", hash_old, hash_new, "--", file_path })
  raw = string.trim(raw)
  if raw == "" then
    return "### 🟢 No Differences Found"
  end
  local path=string.gsub(file_path,".md","")
  local old="[[git:"..path.."_"..hash_old.."|".. hash_old .."]]"
  local new="[[git:"..path.."_"..hash_new.."|".. hash_new .."]]"
  return  "### 🔍[["..path.. "]] : diff between " .. old .. " and " .. new .."\n" .."${gitdiff_render(\"".. encoding.base64Encode(raw).."\")}"
end

-- ===========================================================================
-- == Helpers for virtual refs
-- ===========================================================================
---
-- Parse a virtual ref "path_hash" and fetch content.
-- returns { path=..., hash=..., content=... }
local function get_content(ref)
  -- FIX: Use string.match instead of string.split.
  -- Pattern "^(.*)_(.*)$":
  --   ^(.*) : Greedily matches everything from the start (swallowing underscores in filenames).
  --   _     : Matches the LAST underscore (because the first capture was greedy).
  --   (.*)$ : Matches everything after the last underscore (the hash).
  local path, hash = string.match(ref, "^(.*)_(.*)$")
  
  if path and hash then
    local ok, content = pcall(get_file_contents, path .. ".md", hash)
    if not ok then
      log("get_content error for", ref, content)
      return { path = path, hash = hash, content = nil }
    end
    return { path = path, hash = hash, content = content }
  end
  
  -- Fallback for safety, though likely not needed with the regex above
  return { path = nil, hash = nil, content = nil }
end

-- ===========================================================================
-- == Virtual Pages
-- ===========================================================================
virtualPage.define {
  pattern = "git:(.+)",
  run = function(ref)
    local result = get_content(ref).content
    if result == nil then
      editor.flashNotification("Path " .. ref .. " corrupted", "error")
    end
    return result
  end
}

virtualPage.define {
  pattern = "git status",
  run = function()
    return get_git_status()
  end
}

virtualPage.define {
  pattern = "diff:(.+)",
  run = function(ref)
    -- Note: This splits by comma to separate the two commit refs.
    -- If your FILENAME contains a comma, this will still break.
    -- But the underscore issue is fixed by get_content.
    local data = string.split(ref, ",")
    if #data > 1 then
      local from = get_content(data[1])
      local to = get_content(data[2])
      if not from or not to or not from.hash or not to.hash or not from.path then
        editor.flashNotification("Path " .. ref .. " corrupted", "error")
        return nil
      end
      return get_diff_between_commits(from.hash, to.hash, from.path .. ".md")
    end
    editor.flashNotification("Path " .. ref .. " corrupted", "error")
    return nil
  end
}

-- ===========================================================================
-- == Commands
-- ===========================================================================
---
-- Browse commit history and open a commit.
command.define {
  name = "Git: History",
  run = function()
    local file_path = editor.getCurrentPage() .. ".md"
    local history = get_history(file_path)
    if not history or #history == 0 then
      editor.flashNotification("No git history for " .. file_path, "info")
      return
    end
    local selected = editor.filterBox("📜 Git History", history, "🔍 Select a commit", "Type to search...")
    if selected and selected.ref then
      editor.navigate("git:" .. selected.ref)
    end
  end
}

---
-- Select two commits and show their diff.
command.define {
  name = "Git: Diff",
  run = function()
    local file_path = editor.getCurrentPage() .. ".md"
    local history = get_history(file_path)
    if not history or #history == 0 then
      editor.flashNotification("No git history for " .. file_path, "info")
      return
    end
    local from = editor.filterBox("📜 Git History", history, "🔍 Select 1st commit")
    local to = editor.filterBox("📜 Git History", history, "🔍 Select 2nd commit")
    if from and to and from.ref and to.ref then
      editor.navigate("diff:" .. from.ref .. "," .. to.ref)
    end
  end
}

---
-- Restore a chosen commit to the current buffer.
command.define {
  name = "Git: Restore",
  run = function()
    local file_path = editor.getCurrentPage() .. ".md"
    local history = get_history(file_path)
    if not history or #history == 0 then
      editor.flashNotification("No git history for " .. file_path, "info")
      return
    end
    local selected = editor.filterBox("♻️ Restore", history, "Select commit to restore")
    if not selected or not selected.ref then
      return
    end
    local data = get_content(selected.ref)
    if not data or not data.content then
      editor.flashNotification("Could not restore this commit", "error")
      return
    end
    editor.setText(data.content)
    editor.flashNotification("Commit restored: " .. (data.hash or "unknown"), "success")
  end
}

---
-- Show git status in a virtual page.
command.define {
  name = "Git: Status",
  run = function()
    editor.navigate("git status")
  end
}

1 Like