Skip to content

Commit e74b135

Browse files
committed
feat(snapshot): make snapshot path more reliable on windows
This should fix windows as it does not rely on the shell to do an sha1 This should fix #404
1 parent f598a35 commit e74b135

7 files changed

Lines changed: 174 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ require('opencode').setup({
123123
default_system_prompt = nil, -- Custom system prompt to use for all sessions. If nil, uses the default built-in system prompt
124124
keymap_prefix = '<leader>o', -- Default keymap prefix for global keymaps change to your preferred prefix and it will be applied to all keymaps starting with <leader>o
125125
opencode_executable = 'opencode', -- Name of your opencode binary
126+
snapshot_path = nil, -- Override base path for the snapshot git directory (default: $XDG_DATA_HOME/opencode). Appends /snapshot/<project_id>/<worktree_hash>
126127

127128
-- Server configuration for custom/external opencode servers
128129
server = {

lua/opencode/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ M.defaults = {
301301
},
302302
prompt_guard = nil,
303303
child_readonly = true,
304+
snapshot_path = nil,
304305
hooks = {
305306
on_file_edited = nil,
306307
on_session_loaded = nil,

lua/opencode/config_file.lua

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,29 @@ local function sha1(str)
5151
end
5252

5353
---Get the snapshot storage path for the current workspace
54+
---Matches opencode's Global.Path.data + "snapshot" + projectId + Hash.fast(worktree)
55+
---Can be overridden via config.snapshot_path (base path, project_id and worktree_hash are appended)
5456
---@type fun(): Promise<string>
5557
M.get_workspace_snapshot_path = Promise.async(function()
5658
local project = M.get_opencode_project():await() --[[@as OpencodeProject|nil]]
5759
if not project then
5860
return ''
5961
end
60-
local home = vim.uv.os_homedir()
62+
local data_home = require('opencode.config').snapshot_path
63+
if not data_home or data_home == '' then
64+
data_home = vim.uv.os_getenv('XDG_DATA_HOME')
65+
if not data_home or data_home == '' then
66+
data_home = vim.uv.os_homedir() .. '/.local/share'
67+
end
68+
data_home = vim.fs.joinpath(data_home, 'opencode')
69+
end
6170
local cwd = vim.fn.getcwd()
6271
local worktree_hash = sha1(cwd)
6372
if not worktree_hash then
6473
return ''
6574
end
66-
return home .. '/.local/share/opencode/snapshot/' .. project.id .. '/' .. worktree_hash
75+
local path = vim.fs.joinpath(data_home, 'snapshot', project.id, worktree_hash)
76+
return vim.fs.normalize(path)
6777
end)
6878

6979
local _providers_render_callback = false

lua/opencode/sha1.lua

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
local band = bit.band
2+
local bor = bit.bor
3+
local bxor = bit.bxor
4+
local bnot = bit.bnot
5+
local rol = bit.rol
6+
7+
local function u32hex(n)
8+
if n < 0 then
9+
n = n + 4294967296
10+
end
11+
return string.format('%08x', n)
12+
end
13+
14+
---@param str string
15+
---@return string|nil
16+
local function sha1(str)
17+
local bytes = { string.byte(str, 1, #str) }
18+
local bit_len = #bytes * 8
19+
20+
bytes[#bytes + 1] = 0x80
21+
while (#bytes % 64) ~= 56 do
22+
bytes[#bytes + 1] = 0
23+
end
24+
25+
local bit_len_hi = math.floor(bit_len / 4294967296)
26+
local bit_len_lo = bit_len % 4294967296
27+
28+
bytes[#bytes + 1] = math.floor(bit_len_hi / 16777216) % 256
29+
bytes[#bytes + 1] = math.floor(bit_len_hi / 65536) % 256
30+
bytes[#bytes + 1] = math.floor(bit_len_hi / 256) % 256
31+
bytes[#bytes + 1] = band(bit_len_hi, 0x000000ff)
32+
bytes[#bytes + 1] = math.floor(bit_len_lo / 16777216) % 256
33+
bytes[#bytes + 1] = math.floor(bit_len_lo / 65536) % 256
34+
bytes[#bytes + 1] = math.floor(bit_len_lo / 256) % 256
35+
bytes[#bytes + 1] = band(bit_len_lo, 0x000000ff)
36+
37+
local h0 = 0x67452301
38+
local h1 = 0xefcdab89
39+
local h2 = 0x98badcfe
40+
local h3 = 0x10325476
41+
local h4 = 0xc3d2e1f0
42+
43+
for i = 1, #bytes, 64 do
44+
local w = {}
45+
for j = 1, 16 do
46+
local k = i + (j - 1) * 4
47+
w[j] = band(bytes[k] * 16777216 + bytes[k + 1] * 65536 + bytes[k + 2] * 256 + bytes[k + 3], 0xffffffff)
48+
end
49+
for j = 17, 80 do
50+
w[j] = rol(bxor(bxor(w[j - 3], w[j - 8]), bxor(w[j - 14], w[j - 16])), 1)
51+
end
52+
53+
local a = h0
54+
local b = h1
55+
local c = h2
56+
local d = h3
57+
local e = h4
58+
59+
for j = 1, 80 do
60+
local f, k
61+
if j <= 20 then
62+
f = bor(band(b, c), band(bnot(b), d))
63+
k = 0x5a827999
64+
elseif j <= 40 then
65+
f = bxor(bxor(b, c), d)
66+
k = 0x6ed9eba1
67+
elseif j <= 60 then
68+
f = bor(bor(band(b, c), band(b, d)), band(c, d))
69+
k = 0x8f1bbcdc
70+
else
71+
f = bxor(bxor(b, c), d)
72+
k = 0xca62c1d6
73+
end
74+
75+
local temp = band(rol(a, 5) + f + e + k + w[j], 0xffffffff)
76+
e = d
77+
d = c
78+
c = rol(b, 30)
79+
b = a
80+
a = temp
81+
end
82+
83+
h0 = band(h0 + a, 0xffffffff)
84+
h1 = band(h1 + b, 0xffffffff)
85+
h2 = band(h2 + c, 0xffffffff)
86+
h3 = band(h3 + d, 0xffffffff)
87+
h4 = band(h4 + e, 0xffffffff)
88+
end
89+
90+
return u32hex(h0) .. u32hex(h1) .. u32hex(h2) .. u32hex(h3) .. u32hex(h4)
91+
end
92+
93+
return sha1

lua/opencode/snapshot.lua

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ local function snapshot_git(cmd_args, opts)
1919
local cwd = vim.fn.getcwd()
2020
local args = { 'git', '--git-dir', snapshot_dir, '--work-tree', cwd }
2121
vim.list_extend(args, cmd_args)
22+
2223
local result = vim.system(args, opts or { cwd = cwd }):wait()
2324
if result and result.code == 0 then
2425
return vim.trim(result.stdout), nil
@@ -172,6 +173,7 @@ end
172173

173174
function M.diff_file(snapshot_id, file_path)
174175
local relative_path = vim.fn.fnamemodify(file_path, ':.')
176+
relative_path = relative_path:gsub('\\', '/')
175177
local file_at_snapshot = snapshot_git({ 'show', snapshot_id .. ':' .. relative_path })
176178
local temp_file = write_to_temp_file(file_at_snapshot or '')
177179
local file_type = vim.fn.fnamemodify(file_path, ':e')
@@ -187,7 +189,8 @@ function M.revert(snapshot_id)
187189
end
188190
local deleted_files = {}
189191
for _, file in ipairs(patch_result.files) do
190-
local relative_path = file:match('^' .. vim.fn.getcwd() .. '/?(.*)$')
192+
local relative_path = file:match('^' .. vim.pesc(vim.fn.getcwd()) .. '/?(.*)$')
193+
relative_path = relative_path:gsub('\\', '/')
191194
local res, err = snapshot_git({ 'checkout', snapshot_id, '--', relative_path })
192195
if not res then
193196
vim.notify(

lua/opencode/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@
389389
---@field child_readonly boolean
390390
---@field hooks OpencodeHooks
391391
---@field quick_chat OpencodeQuickChatConfig
392+
---@field snapshot_path? string -- Override base path for snapshot storage (default: $XDG_DATA_HOME/opencode). Appends /snapshot/<project_id>/<worktree_hash>
392393

393394
---@class MessagePartState
394395
---@field input TaskToolInput|BashToolInput|FileToolInput|TodoToolInput|GlobToolInput|GrepToolInput|WebFetchToolInput|ListToolInput|QuestionToolInput|ApplyPatchToolInput Input data for the tool

tests/unit/sha1_spec.lua

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
local sha1 = require('opencode.sha1')
2+
3+
describe('sha1', function()
4+
it('produces correct hash for empty string', function()
5+
assert.equals('da39a3ee5e6b4b0d3255bfef95601890afd80709', sha1(''))
6+
end)
7+
8+
it('produces correct hash for "abc"', function()
9+
assert.equals('a9993e364706816aba3e25717850c26c9cd0d89d', sha1('abc'))
10+
end)
11+
12+
it('produces correct hash for "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"', function()
13+
assert.equals('84983e441c3bd26ebaae4aa1f95129e5e54670f1', sha1('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'))
14+
end)
15+
16+
it('produces correct hash for single character', function()
17+
assert.equals('86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', sha1('a'))
18+
end)
19+
20+
it('produces correct hash for "ab"', function()
21+
assert.equals('da23614e02469a0d7c7bd1bdab5c9c474b1904dc', sha1('ab'))
22+
end)
23+
24+
it('produces correct hash for numbers', function()
25+
assert.equals('01b307acba4f54f55aafc33bb06bbbf6ca803e9a', sha1('1234567890'))
26+
end)
27+
28+
it('produces correct hash for special characters', function()
29+
assert.equals('bf24d65c9bb05b9b814a966940bcfa50767c8a8d', sha1('!@#$%^&*()'))
30+
end)
31+
32+
it('produces correct hash for string with spaces', function()
33+
assert.equals('2aae6c35c94fcfb415dbe95f408b9ce91ee846ed', sha1('hello world'))
34+
end)
35+
36+
it('produces correct hash for newlines', function()
37+
assert.equals('05eed6236c8bda5ecf7af09bae911f9d5f90998b', sha1('line1\nline2'))
38+
end)
39+
40+
it('produces correct hash for null byte', function()
41+
assert.equals('dbdd4f85d8a56500aa5c9c8a0d456f96280c92e5', sha1('ab\0c'))
42+
end)
43+
44+
it('produces correct hash for 448-bit message (exactly 56 bytes, boundary condition)', function()
45+
local msg = string.rep('a', 56)
46+
assert.equals('c2db330f6083854c99d4b5bfb6e8f29f201be699', sha1(msg))
47+
end)
48+
49+
it('produces correct hash for 512-bit message (exactly 64 bytes, one full block)', function()
50+
local msg = string.rep('a', 64)
51+
assert.equals('0098ba824b5c16427bd7a1122a5a442a25ec644d', sha1(msg))
52+
end)
53+
54+
it('produces correct hash for multi-block message (200 bytes)', function()
55+
local msg = string.rep('a', 200)
56+
assert.equals('e61cfffe0d9195a525fc6cf06ca2d77119c24a40', sha1(msg))
57+
end)
58+
59+
it('produces correct hash for unicode text', function()
60+
assert.equals('24e9f5c07847ff8a2a9fa77456655792f5bc7f9f', sha1('héllo wörld'))
61+
end)
62+
end)

0 commit comments

Comments
 (0)