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

**URL:** https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539
**Category:** Plugs and Libraries
**Created:** [November 14, 2025, 2:32pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539 "2025-11-14T14:32:52Z")
**Posts on this page:** 16
**Page:** 1

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [November 14, 2025, 2:32pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/1 "2025-11-14T14:32:52Z")

</div>

![git](https://community.silverbullet.md/uploads/default/original/2X/4/4ffd583d557a676df471d042e0793c614e742b77.gif)

# Git History Module

## 🎯 Description

This module adds full **Git integration** into SilverBullet / Space-Lua.

It provides:

- 🕘 **Commit history browser** for the current `.md` file
- 🧾 **View file contents** at any commit
- 🔍 **Compare (diff)** between two commits, rendered in Markdown
- ♻ **Restore** any commit into the active editor buffer
- 📌 **View Git status** as clean, emoji-enhanced Markdown
- 📄 **Virtual pages** to navigate Git history like normal documents

> **warning** Warning  
> Depends on:  
> `git` CLI  
> `utilities.debug` helper

* * *

## ⚙ How It Works

### ✔ Virtual Pages

The module registers custom pages:

| Virtual Page Pattern | Purpose |
| --- | --- |
| `git:<path_hash>` | Displays file content at a specific commit |
| `diff:<ref1>,<ref2>` | Shows a diff between two commits |
| `git status` | Shows Git status in Markdown |

The user can navigate to these pages just like regular documents.

* * *

### ✔ Commands

The module adds SilverBullet commands:

| Command | Action |
| --- | --- |
| **Git: History** | Pick a commit and view its content |
| **Git: Diff** | Pick 2 commits → view diff |
| **Git: Status** | Display `git status` |
| **Git: Restore** | Load an old commit into the editor |

Everything is interactive using `editor.filterBox()` and `editor.navigate()`.

* * *

### ✔ Markdown Rendering

The module converts raw Git output to readable Markdown:

- 🟢 additions
- 🔴 deletions
- 🟦 context lines
- 🆕 untracked files
- 🟡 unstaged modifications
- 🟠 staged modifications
- 🔁 renamed files

* * *

# 🧩 Code

```space-lua
-- ###########################################################################
-- ## Git History Module (complete, restored)
-- ## 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
     -- utilities.debug(table.concat({...}, " "))
  end
end

local source = config.get("history.source")
if source == nil then
  editor.flashNotification("'marp.source' configuration not set", "error")
end

local current_panel_id = "rhs"
local is_panel_visible = false

-- ===========================================================================
-- == 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),
          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)
  local data = string.split(ref, "_")
  if #data > 1 then
    local path = data[1]
    local hash = data[2]
    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
  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)
    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
}

```

## Renderers

```space-lua
-- Render `git status --porcelain` into readable Markdown.
-- Uses emojis to indicate status.
function gitstatus_render(raw)
  if not raw or raw == "" then
    return "### 🟢 Clean Working Tree\nNo changes."
  end

  local md = { "### 📌 Git Status\n" }

  for line in raw:gmatch("[^\r\n]+") do
    local code = line:sub(1, 2)
    local path = string.trim(line:sub(3) or "")

    if code == "??" then
      table.insert(md, "🆕 Untracked: " .. path)
    elseif code == " M" then
      table.insert(md, "🟡 Modified (unstaged): " .. path)
    elseif code == "M " then
      table.insert(md, "🟠 Modified (staged): " .. path)
    elseif code == " D" then
      table.insert(md, "🔴 Deleted (unstaged): " .. path)
    elseif code == "D " then
      table.insert(md, "🛑 Deleted (staged): " .. path)
    elseif code == "A " or code == " A" then
      table.insert(md, "🟢 Added: " .. path)
    elseif code == "R " or code == " R" then
      table.insert(md, "🔁 Renamed: " .. path)
    elseif code == "C " or code == " C" then
      table.insert(md, "📄 Copied: " .. path)
    else
      table.insert(md, "🟦 " .. code .. " " .. path)
    end
  end

  return table.concat(md, "\n")
end

-- Render raw git diff.
function gitdiff_render(base64Text)
  local diffText= encoding.utf8Decode(encoding.base64Decode(base64Text))
  local lines = string.split(diffText, "\n")  
  local html = '<pre class="git-diff">'  
    
  for _, line in ipairs(lines) do  
    if string.startsWith(line, "---") or string.startsWith(line, "+++") then  
      html = html .. '<div class="diff-header">' .. line .. '</div>'  
    elseif string.startsWith(line, "@@") then  
      html = html .. '<div class="diff-hunk">' .. line .. '</div>'  
    elseif string.startsWith(line, "-") then  
      html = html .. '<div class="diff-delete">' .. line .. '</div>'  
    elseif string.startsWith(line, "+") then  
      html = html .. '<div class="diff-add">' .. line .. '</div>'  
    else  
      html = html .. '<div class="diff-context">' .. line .. '</div>'  
    end  
  end  
  html = html..'</pre>'  
  return widget.htmlBlock(html)
end

```

## CSS

```space-style
.git-diff {  
  font-family: monospace;  
  margin: 0;  
}  
.diff-header { color: #888; font-weight: bold; }  
.diff-hunk { color: #0066cc; }  
.diff-delete { background-color: #ffdddd; color: #cc0000; }  
.diff-add { background-color: #ddffdd; color: #00cc00; }  
.diff-context { color: inherit; }

```

---

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [November 14, 2025, 2:36pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/2 "2025-11-14T14:36:20Z")

</div>

I have not published this script in my library because I have not migrated to new library manager and I think that it will be interesting to integrate those features into a lua git plugin like @zef or @ChenZhu-Xie to have a full-featured git plugins.

What do you think?

PD: I’m not totally satisfied by renders but it’s currently correct.

---

<div class="post-metadata">

### Author: ![zef](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zef/32/7_2.png) [@zef](https://community.silverbullet.md/u/zef)
#### Post date: [November 14, 2025, 2:58pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/3 "2025-11-14T14:58:38Z")

</div>

This is very cool looking!

Yeah, we can take two paths here, either we try to create the ultimate Git library, or we all contribute our own parts.

I’m more tempted to each have our own, allowing us freedom what to add and when, but ideally not overlapping too much in functionality. I will be archiving the [GitHub - silverbulletmd/silverbullet-libraries: Awesome SilverBullet libraries](https://github.com/silverbulletmd/silverbullet-libraries) repo I think, and publish some libraries (like the basic Git one) under my own name here: [GitHub - zefhemel/silverbullet-libraries: My collection of libraries and plugs](https://github.com/zefhemel/silverbullet-libraries)

You can do the same, and we can create a curated list of them in a repository (or you in your own repo).

---

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [November 14, 2025, 6:03pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/4 "2025-11-14T18:03:45Z")

</div>

```lua
virtualPage.define {
  pattern = "diff:(.+)",
  run = function(ref)
    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
}

local function diff_render(diffText)
   local lines = string.split(diffText, "\n")  
  local html = '<pre class="git-diff">'  
    
  for _, line in ipairs(lines) do  
    if string.startsWith(line, "---") or string.startsWith(line, "+++") then  
      html = html .. '<div class="diff-header">' .. line .. '</div>'  
    elseif string.startsWith(line, "@@") then  
      html = html .. '<div class="diff-hunk">' .. line .. '</div>'  
    elseif string.startsWith(line, "-") then  
      html = html .. '<div class="diff-delete">' .. line .. '</div>'  
    elseif string.startsWith(line, "+") then  
      html = html .. '<div class="diff-add">' .. line .. '</div>'  
    else  
      html = html .. '<div class="diff-context">' .. line .. '</div>'  
    end  
  end  
    
  html = html..'</pre>'  
  return widget.htmlBlock(html)
end

```

if `get_diff_between_commits` return a widget, SB returns a strange error `("t.doc || "").split is not a function`. Is it a bug?  
widget is working calling it directly  
virtualPage supports widget? @zef

---

<div class="post-metadata">

### Author: ![zef](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zef/32/7_2.png) [@zef](https://community.silverbullet.md/u/zef)
#### Post date: [November 14, 2025, 6:07pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/5 "2025-11-14T18:07:03Z")

</div>

The `run` function has to return a plain markdown string. In this markdown you can include `${...}` again.

---

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [November 15, 2025, 11:33am UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/6 "2025-11-15T11:33:42Z")

</div>

git diff renderer works fine ! Thanks @zef

 ![image](https://community.silverbullet.md/uploads/default/original/2X/c/cba171a1a00459e84fcb472067c20e0751e747a3.png)

---

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [November 15, 2025, 4:51pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/7 "2025-11-15T16:51:53Z")

</div>

To install:  
Navigate to your Library Manager inside Silverbullet  
Add my repository: [silverbullet-libraries/Repository/Malys.md at main · malys/silverbullet-libraries · GitHub](https://github.com/malys/silverbullet-libraries/blob/main/Repository/Malys.md)  
Add any script my repository

---

<div class="post-metadata">

### Author: ![zeus-web](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zeus-web/32/571_2.png) [@zeus-web](https://community.silverbullet.md/u/zeus-web)
#### Post date: [November 15, 2025, 8:52pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/8 "2025-11-15T20:52:04Z")

</div>

This does look great, but unfortunately I can’t install it. I am able to add your repo without problems, but all Libs I tried to install fail. with error:  
“Lua error: Could not fetch: github:malys/silverbullet-libraries/GitHistory.md”  
(or similar, depending in filename).

I tried with most recent SB Version (SilverBullet 2.2.1-52-g8c34b35e). And e.g. I can install Libs from Mr.Red without problem. So I assume, there might be an error in your github structure? Not sure…

---

<div class="post-metadata">

### Author: ![zeus-web](https://community.silverbullet.md/user_avatar/community.silverbullet.md/zeus-web/32/571_2.png) [@zeus-web](https://community.silverbullet.md/u/zeus-web)
#### Post date: [November 15, 2025, 9:00pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/9 "2025-11-15T21:00:09Z")

</div>

Nevermind. Just found out, Xiè Chén-Zhú posted the solution in the “md Table render” thread 🙂

---

<div class="post-metadata">

### Author: ![ChenZhu-Xie](https://community.silverbullet.md/user_avatar/community.silverbullet.md/chenzhu-xie/32/1476_2.png) [@ChenZhu-Xie](https://community.silverbullet.md/u/ChenZhu-Xie)
#### Post date: [November 17, 2025, 10:53am UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/10 "2025-11-17T10:53:27Z")

</div>

GIT PLUG all functioning on linux machine:

 ![image](https://community.silverbullet.md/uploads/default/original/2X/1/130a7f0cf843db46f5c971352e45e75985924712.jpeg)

 ![image](https://community.silverbullet.md/uploads/default/original/2X/9/9ce52fb7ca45aa0d3cfc9db9066e1e070213ab74.png)

but on windows machine, most git cmds failed for

`Failed to navigate: Cannot read properties of undefined (reading 'lastModified')`

I think this is a rather common error, although I haven’t located it yet -\_-||

---

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [November 17, 2025, 1:31pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/11 "2025-11-17T13:31:27Z")

</div>

Could you investigate and propose me a patch to integrate?

---

<div class="post-metadata">

### Author: ![ChenZhu-Xie](https://community.silverbullet.md/user_avatar/community.silverbullet.md/chenzhu-xie/32/1476_2.png) [@ChenZhu-Xie](https://community.silverbullet.md/u/ChenZhu-Xie)
#### Post date: [November 17, 2025, 9:01pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/12 "2025-11-17T21:01:11Z")

</div>

Of course, I will further investigate where the problem lies.

---

<div class="post-metadata">

### Author: ![ChenZhu-Xie](https://community.silverbullet.md/user_avatar/community.silverbullet.md/chenzhu-xie/32/1476_2.png) [@ChenZhu-Xie](https://community.silverbullet.md/u/ChenZhu-Xie)
#### Post date: [November 18, 2025, 11:50am UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/13 "2025-11-18T11:50:18Z")

</div>

the issue seems to originate here:

```auto
-- 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.flashNotification(selected.ref)
      editor.navigate("git:" .. selected.ref) -- <=========
    end
  end
}

```

that is here:

```auto
${editor.navigate("git:Library/Malys/GitHistory_930bab41")}

```

thus here:

```auto
virtualPage.define {
  pattern = "git:(.+)",
  run = function(ref)
    -- print("dddddddddd") <============ however I cannot even get this
    -- editor.flashNotification("dddddddddd") -- <========= nothing happened
    local result = get_content(ref).content
    if result == nil then
      editor.flashNotification("Path " .. ref .. " corrupted", "error")
    end
    return result
  end
}

```

Strange thing is that I can get neither `print` from console, nor `flashNotification` from editor, when I open `git:Library/Malys/GitHistory_930bab41` as a VirtualPage…

the console:

```auto
[Service Worker] No local copy of git:Library/Malys/GitHistory_930bab41.md proxying to server
injected.js:1  
GET http://127.0.0.1:3000/.fs/git%3ALibrary/Malys/GitHistory_930bab41.md 500 (Internal Server Error)
client.js?v=cache-1762962938998:29 
[Client] Now navigating to Library/Malys/GitHistory

```

My guess is it’s the file path, so simply: ` :gsub("/", "\\")` enough? But somehow, it didn’t work -\_-||.

Emm, better we communicate through DMs?

---

<div class="post-metadata">

### Author: ![ChenZhu-Xie](https://community.silverbullet.md/user_avatar/community.silverbullet.md/chenzhu-xie/32/1476_2.png) [@ChenZhu-Xie](https://community.silverbullet.md/u/ChenZhu-Xie)
#### Post date: [November 18, 2025, 3:53pm UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/14 "2025-11-18T15:53:32Z")

</div>

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: history`ed.

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:

```auto
-- ###########################################################################
-- ## 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
}

```

---

<div class="post-metadata">

### Author: ![malys](https://community.silverbullet.md/user_avatar/community.silverbullet.md/malys/32/840_2.png) [@malys](https://community.silverbullet.md/u/malys)
#### Post date: [January 24, 2026, 10:27am UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/15 "2026-01-24T10:27:51Z")

</div>

Bug fixes (regression)  
See changelog in md file

---

<div class="post-metadata">

### Author: ![hurifiva](https://community.silverbullet.md/letter_avatar_proxy/v4/letter/h/a698b9/32.png) [@hurifiva](https://community.silverbullet.md/u/hurifiva)
#### Post date: [August 30, 2026, 4:22am UTC](https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539/16 "2026-08-30T04:22:51Z")

</div>

Now that multiple users have dropped in 2.10, is there a way to have a different git user for different SB user? So that I can see who made which change.
