Skip to content

Commit 41a5fff

Browse files
committed
Add support for symlinks
Closes #27
1 parent 8d513e4 commit 41a5fff

7 files changed

Lines changed: 170 additions & 49 deletions

File tree

lua/hunk/api/changeset.lua

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ local function merge_lists(a, b)
88
local seen = {}
99

1010
local function add_unique(list)
11-
for _, item in ipairs(list) do
12-
if not seen[item] then
13-
seen[item] = true
11+
for path in pairs(list) do
12+
if not seen[path] then
13+
seen[path] = true
1414
end
1515
end
1616
end
@@ -22,40 +22,42 @@ local function merge_lists(a, b)
2222
end
2323

2424
function M.load_changeset(left, right)
25-
local left_files = fs.list_files_recursively(left)
26-
local right_files = fs.list_files_recursively(right)
25+
local left_files = fs.scan_dir(left)
26+
local right_files = fs.scan_dir(right)
2727
local files = merge_lists(left_files, right_files)
2828

2929
local changeset = {}
3030

3131
for _, file in ipairs(files) do
32-
local has_left = utils.included_in_table(left_files, file)
33-
local has_right = utils.included_in_table(right_files, file)
32+
local left_file = left_files[file]
33+
local right_file = right_files[file]
3434

3535
local type = "modified"
36-
if not has_left then
36+
if not left_file then
37+
left_file = {}
3738
type = "added"
3839
end
39-
if not has_right then
40+
if not right_file then
41+
right_file = {}
4042
type = "deleted"
4143
end
4244

43-
local left_filepath = left .. "/" .. file
44-
local right_filepath = right .. "/" .. file
45+
left_file.path = left .. "/" .. file
46+
right_file.path = right .. "/" .. file
4547

4648
changeset[file] = {
4749
type = type,
4850

49-
left_filepath = left_filepath,
50-
right_filepath = right_filepath,
51+
left_file = left_file,
52+
right_file = right_file,
5153
filepath = file,
5254

5355
selected = false,
5456
selected_lines = {
5557
left = {},
5658
right = {},
5759
},
58-
hunks = diff.diff_file(left_filepath, right_filepath),
60+
hunks = diff.diff_file(left_file, right_file),
5961
}
6062
end
6163

@@ -67,7 +69,7 @@ local function write_change(change, output_dir)
6769
local output_file = output_dir .. "/" .. change.filepath
6870

6971
if change.type == "deleted" and not change.selected and not any_selected then
70-
fs.copy_file(change.left_filepath, output_file)
72+
fs.move_file(change.left_file.path, output_file)
7173
return
7274
end
7375

@@ -82,19 +84,19 @@ local function write_change(change, output_dir)
8284
end
8385

8486
if change.selected and change.type ~= "deleted" then
85-
fs.copy_file(change.right_filepath, output_file)
87+
fs.move_file(change.right_file.path, output_file)
8688
return
8789
end
8890

8991
if any_selected then
90-
local left_file_content = fs.read_file_as_lines(change.left_filepath)
91-
local right_file_content = fs.read_file_as_lines(change.right_filepath)
92+
local left_file_content = fs.read_file_as_lines(change.left_file.path)
93+
local right_file_content = fs.read_file_as_lines(change.right_file.path)
9294
local result = diff.apply_diff(left_file_content, right_file_content, change)
9395
fs.write_file(output_dir .. "/" .. change.filepath, result)
9496
return
9597
end
9698

97-
fs.copy_file(change.left_filepath, output_file)
99+
fs.move_file(change.left_file.path, output_file)
98100
end
99101

100102
function M.write_changeset(changeset, output_dir)

lua/hunk/api/diff.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ local fs = require("hunk.api.fs")
44
local M = {}
55

66
function M.diff_file(left, right)
7-
local left_content = fs.read_file(left) or ""
8-
local right_content = fs.read_file(right) or ""
7+
if left.symlink or right.symlink then
8+
return {}
9+
end
10+
11+
local left_content = fs.read_file(left.path) or ""
12+
local right_content = fs.read_file(right.path) or ""
913
local hunks = vim.diff(left_content, right_content, {
1014
result_type = "indices",
1115
})

lua/hunk/api/fs.lua

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,47 @@
11
local M = {}
22

3-
function M.list_files_recursively(dir)
4-
local files = {}
5-
local p = io.popen('find "' .. dir .. '" -type f')
6-
if not p then
7-
return {}
3+
local uv = vim.loop
4+
5+
function M.scan_dir(dir)
6+
dir = (dir:gsub("/+$", ""))
7+
local out = {}
8+
9+
local function relative_path(full)
10+
return full:sub(#dir + 2)
811
end
9-
for file in p:lines() do
10-
table.insert(files, file)
12+
13+
local function walk(path)
14+
local fd = uv.fs_scandir(path)
15+
if not fd then
16+
return
17+
end
18+
19+
while true do
20+
local name, ftype = uv.fs_scandir_next(fd)
21+
if not name then
22+
break
23+
end
24+
25+
local full = path .. "/" .. name
26+
27+
if ftype == "directory" then
28+
walk(full)
29+
elseif ftype == "file" then
30+
local file_path = relative_path(full)
31+
out[file_path] = { path = file_path }
32+
elseif ftype == "link" then
33+
local file_path = relative_path(full)
34+
out[file_path] = {
35+
path = file_path,
36+
symlink = uv.fs_readlink(full),
37+
}
38+
end
39+
end
1140
end
12-
p:close()
13-
return vim.tbl_map(function(file)
14-
return string.sub(file, #dir + 2)
15-
end, files)
41+
42+
walk(dir)
43+
44+
return out
1645
end
1746

1847
function M.read_file(file_path)
@@ -38,9 +67,9 @@ function M.make_parents(file_path)
3867
vim.fn.mkdir(parent_dir, "p")
3968
end
4069

41-
function M.copy_file(src, dst)
70+
function M.move_file(src, dst)
4271
M.make_parents(dst)
43-
vim.fn.system({ "cp", src, dst })
72+
vim.fn.system({ "mv", src, dst })
4473
end
4574

4675
function M.rm_file(file)

lua/hunk/ui/file.lua

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,21 @@ local function create_buffer(params)
4545

4646
buf = vim.api.nvim_create_buf(false, false)
4747

48-
local lines = api.fs.read_file_as_lines(params.change[params.side .. "_filepath"])
48+
local file
49+
if params.side == "left" then
50+
file = params.change.left_file
51+
elseif params.side == "right" then
52+
file = params.change.right_file
53+
else
54+
error("Unknown side " .. params.side)
55+
end
56+
57+
local lines = {}
58+
if file.symlink then
59+
lines = { file.symlink }
60+
else
61+
lines = api.fs.read_file_as_lines(file.path)
62+
end
4963
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
5064

5165
for key, value in pairs(bufopts) do

tests/hunk/diff_spec.lua

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ describe("diff patching", function()
99

1010
it("should do nothing if no lines are selected", function()
1111
local change = changeset.modified
12-
local left_file_content = api.fs.read_file_as_lines(change.left_filepath)
13-
local right_file_content = api.fs.read_file_as_lines(change.right_filepath)
12+
local left_file_content = api.fs.read_file_as_lines(change.left_file.path)
13+
local right_file_content = api.fs.read_file_as_lines(change.right_file.path)
1414
local result = api.diff.apply_diff(left_file_content, right_file_content, change)
1515
assert.are.same(left_file_content, result)
1616
end)
1717

1818
it("should apply left before right", function()
1919
local change = changeset.modified
20-
local left_file_content = api.fs.read_file_as_lines(change.left_filepath)
21-
local right_file_content = api.fs.read_file_as_lines(change.right_filepath)
20+
local left_file_content = api.fs.read_file_as_lines(change.left_file.path)
21+
local right_file_content = api.fs.read_file_as_lines(change.right_file.path)
2222
change.selected_lines = {
2323
left = {
2424
[1] = false,
@@ -42,8 +42,8 @@ describe("diff patching", function()
4242

4343
it("should apply files with no left correctly", function()
4444
local change = changeset.added
45-
local left_file_content = api.fs.read_file_as_lines(change.left_filepath)
46-
local right_file_content = api.fs.read_file_as_lines(change.right_filepath)
45+
local left_file_content = api.fs.read_file_as_lines(change.left_file.path)
46+
local right_file_content = api.fs.read_file_as_lines(change.right_file.path)
4747
change.selected_lines = {
4848
left = {},
4949
right = {
@@ -62,8 +62,8 @@ describe("diff patching", function()
6262

6363
it("should apply files with no right correctly", function()
6464
local change = changeset.deleted
65-
local left_file_content = api.fs.read_file_as_lines(change.left_filepath)
66-
local right_file_content = api.fs.read_file_as_lines(change.right_filepath)
65+
local left_file_content = api.fs.read_file_as_lines(change.left_file.path)
66+
local right_file_content = api.fs.read_file_as_lines(change.right_file.path)
6767
change.selected_lines = {
6868
left = {
6969
[1] = false,
@@ -102,8 +102,8 @@ describe("diff patching", function()
102102
local changeset = api.changeset.load_changeset(workspace.left, workspace.right)
103103

104104
local change = changeset.modified
105-
local left_file_content = api.fs.read_file_as_lines(change.left_filepath)
106-
local right_file_content = api.fs.read_file_as_lines(change.right_filepath)
105+
local left_file_content = api.fs.read_file_as_lines(change.left_file.path)
106+
local right_file_content = api.fs.read_file_as_lines(change.right_file.path)
107107
change.selected_lines = {
108108
left = {
109109
[1] = true,
@@ -151,8 +151,8 @@ describe("diff patching", function()
151151
local changeset = api.changeset.load_changeset(workspace.left, workspace.right)
152152

153153
local change = changeset.filea
154-
local left_file_content = api.fs.read_file_as_lines(change.left_filepath)
155-
local right_file_content = api.fs.read_file_as_lines(change.right_filepath)
154+
local left_file_content = api.fs.read_file_as_lines(change.left_file.path)
155+
local right_file_content = api.fs.read_file_as_lines(change.right_file.path)
156156
change.selected_lines = {
157157
left = {
158158
[2] = true,

tests/hunk/symlink_spec.lua

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
local fixtures = require("tests.utils.fixtures")
2+
local utils = require("hunk.utils")
3+
local fs = require("hunk.api.fs")
4+
local api = require("hunk.api")
5+
6+
local function create_symlink(dst, source)
7+
vim.fn.system({ "ln", "-s", source, dst })
8+
end
9+
10+
local function prepare_symlinks(workspace)
11+
fs.write_file(workspace.left .. "/had-content", { "1", "2", "3" })
12+
create_symlink(workspace.left .. "/was-symlink1", "some-target")
13+
create_symlink(workspace.left .. "/was-symlink2", "some-target")
14+
15+
create_symlink(workspace.right .. "/had-content", "some-target")
16+
fs.write_file(workspace.right .. "/was-symlink1", { "1", "2", "3" })
17+
create_symlink(workspace.right .. "/new-symlink", "some-target2")
18+
19+
create_symlink(workspace.output .. "/had-content", "some-target")
20+
fs.write_file(workspace.output .. "/was-symlink1", { "1", "2", "3" })
21+
create_symlink(workspace.output .. "/new-symlink", "some-target2")
22+
end
23+
24+
describe("symlinks", function()
25+
it("should correctly load a changeset with symlinks", function()
26+
fixtures.with_workspace(function(workspace)
27+
prepare_symlinks(workspace)
28+
29+
local changeset, files = api.changeset.load_changeset(workspace.left, workspace.right)
30+
31+
it("contains all files from both sides of diff", function()
32+
assert.is_true(utils.included_in_table(files, "had-content"), "missing had-content")
33+
assert.is_true(utils.included_in_table(files, "was-symlink1"), "missing was-symlink1")
34+
assert.is_true(utils.included_in_table(files, "was-symlink2"), "missing was-symlink2")
35+
assert.is_true(utils.included_in_table(files, "new-symlink"), "missing new-symlink")
36+
assert.are.equal(#files, 4)
37+
end)
38+
39+
it("creates a correct had-content change", function()
40+
local change = changeset["had-content"]
41+
assert.are.equal(nil, change.left_file.symlink)
42+
assert.are.equal("some-target", change.right_file.symlink)
43+
assert.are.equal("modified", change.type)
44+
assert.are.same({}, change.hunks)
45+
end)
46+
47+
it("creates a correct was-symlink change", function()
48+
local change = changeset["was-symlink1"]
49+
assert.are.equal("some-target", change.left_file.symlink)
50+
assert.are.equal(nil, change.right_file.symlink)
51+
assert.are.equal("modified", change.type)
52+
assert.are.same({}, change.hunks)
53+
end)
54+
55+
it("creates a correct was-symlink change", function()
56+
local change = changeset["was-symlink2"]
57+
assert.are.equal("some-target", change.left_file.symlink)
58+
assert.are.equal(nil, change.right_file.symlink)
59+
assert.are.equal("deleted", change.type)
60+
assert.are.same({}, change.hunks)
61+
end)
62+
63+
it("creates a correct was-symlink change", function()
64+
local change = changeset["new-symlink"]
65+
assert.are.equal(nil, change.left_file.symlink)
66+
assert.are.equal("some-target2", change.right_file.symlink)
67+
assert.are.equal("added", change.type)
68+
assert.are.same({}, change.hunks)
69+
end)
70+
end)
71+
end)
72+
end)

tests/utils/fixtures.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ function M.prepare_simple_workspace(workspace)
4848
end
4949

5050
function M.read_dir(dir)
51-
local files = fs.list_files_recursively(dir)
51+
local files = fs.scan_dir(dir)
5252
local result = {}
53-
for _, file in ipairs(files) do
53+
for file, _ in pairs(files) do
5454
local content = fs.read_file_as_lines(dir .. "/" .. file)
5555
result[file] = content
5656
end

0 commit comments

Comments
 (0)