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
}