Skip to content

Commit 4e164d3

Browse files
committed
implement copy
1 parent 577995c commit 4e164d3

File tree

2 files changed

+267
-8
lines changed

2 files changed

+267
-8
lines changed

lua/plenary/path2.lua

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@
4949
--- `../../Users/test/test_file` and there's no home directory absolute path
5050
--- in this.
5151
---
52-
--- - rename returns new path rather than mutating path
52+
--- - `rename` returns new path rather than mutating path
53+
---
54+
--- - `copy`
55+
--- - drops interactive mode
56+
--- - return value table is pre-flattened
57+
--- - return value table value is `{success: boolean, err: string?}` rather than just `boolean`
5358

5459
local bit = require "plenary.bit"
5560
local uv = vim.loop
@@ -876,7 +881,7 @@ function Path:touch(opts)
876881

877882
if not not opts.parents then
878883
local mode = type(opts.parents) == "number" and opts.parents or nil ---@cast mode number?
879-
_ = Path:new(self:parent()):mkdir { mode = mode, parents = true }
884+
_ = Path:new(self:parent()):mkdir { mode = mode, parents = true, exists_ok = true }
880885
end
881886

882887
local fd, err = uv.fs_open(self:absolute(), "w", opts.mode)
@@ -901,8 +906,8 @@ function Path:rm(opts)
901906
opts.recursive = vim.F.if_nil(opts.recursive, false)
902907

903908
if not opts.recursive or not self:is_dir() then
904-
local ok, err = uv.fs_unlink(self:absolute())
905-
if ok then
909+
local ok, err, code = uv.fs_unlink(self:absolute())
910+
if ok or code == "ENOENT" then
906911
return
907912
end
908913
if self:is_dir() then
@@ -913,15 +918,15 @@ function Path:rm(opts)
913918

914919
for p, dirs, files in self:walk(false) do
915920
for _, file in ipairs(files) do
916-
local _, err = uv.fs_unlink((p / file):absolute())
917-
if err then
921+
local _, err, code = uv.fs_unlink((p / file):absolute())
922+
if err and code ~= "ENOENT" then
918923
error(err)
919924
end
920925
end
921926

922927
for _, dir in ipairs(dirs) do
923-
local _, err = uv.fs_rmdir((p / dir):absolute())
924-
if err then
928+
local _, err, code = uv.fs_rmdir((p / dir):absolute())
929+
if err and code ~= "ENOENT" then
925930
error(err)
926931
end
927932
end
@@ -953,6 +958,63 @@ function Path:rename(opts)
953958
return new_path
954959
end
955960

961+
---@class plenary.Path2.copyOpts
962+
---@field destination string|plenary.Path2 target file path to copy to
963+
---@field recursive boolean? whether to copy folders recursively (default: `false`)
964+
---@field override boolean? whether to override files (default: `true`)
965+
---@field respect_gitignore boolean? skip folders ignored by all detected `gitignore`s (default: `false`)
966+
---@field hidden boolean? whether to add hidden files in recursively copying folders (default: `true`)
967+
---@field parents boolean? whether to create possibly non-existing parent dirs of `opts.destination` (default: `false`)
968+
---@field exists_ok boolean? whether ok if `opts.destination` exists, if so folders are merged (default: `true`)
969+
970+
---@param opts plenary.Path2.copyOpts
971+
---@return {[plenary.Path2]: {success:boolean, err: string?}} # indicating success of copy; nested tables constitute sub dirs
972+
function Path:copy(opts)
973+
opts.recursive = vim.F.if_nil(opts.recursive, false)
974+
opts.override = vim.F.if_nil(opts.override, true)
975+
976+
local dest = self:parent() / opts.destination ---@type plenary.Path2
977+
978+
local success = {} ---@type {[plenary.Path2]: {success: boolean, err: string?}}
979+
980+
if not self:is_dir() then
981+
local ok, err = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not opts.override })
982+
success[dest] = { success = ok or false, err = err }
983+
return success
984+
end
985+
986+
if not opts.recursive then
987+
error(string.format("Warning: %s was not copied as `recursive=false`", self:absolute()))
988+
end
989+
990+
opts.respect_gitignore = vim.F.if_nil(opts.respect_gitignore, false)
991+
opts.hidden = vim.F.if_nil(opts.hidden, true)
992+
opts.parents = vim.F.if_nil(opts.parents, false)
993+
opts.exists_ok = vim.F.if_nil(opts.exists_ok, true)
994+
995+
dest:mkdir { parents = opts.parents, exists_ok = opts.exists_ok }
996+
997+
local scan = require "plenary.scandir"
998+
local data = scan.scan_dir(self.filename, {
999+
respect_gitignore = opts.respect_gitignore,
1000+
hidden = opts.hidden,
1001+
depth = 1,
1002+
add_dirs = true,
1003+
})
1004+
1005+
for _, entry in ipairs(data) do
1006+
local entry_path = Path:new(entry)
1007+
local new_dest = dest / entry_path.name
1008+
-- clear destination as it might be Path table otherwise failing w/ extend
1009+
opts.destination = nil
1010+
local new_opts = vim.tbl_deep_extend("force", opts, { destination = new_dest })
1011+
-- nil: not overriden if `override = false`
1012+
local res = entry_path:copy(new_opts)
1013+
success = vim.tbl_deep_extend("force", success, res)
1014+
end
1015+
return success
1016+
end
1017+
9561018
--- read file synchronously or asynchronously
9571019
---@param callback fun(data: string)? callback to use for async version, nil for default
9581020
---@return string? data

tests/plenary/path2_spec.lua

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,203 @@ describe("Path2", function()
635635
end)
636636
end)
637637

638+
describe("copy", function()
639+
after_each(function()
640+
uv.fs_unlink "a_random_filename.rs"
641+
uv.fs_unlink "not_a_random_filename.rs"
642+
uv.fs_unlink "some_random_filename.rs"
643+
uv.fs_unlink "../some_random_filename.rs"
644+
Path:new("src"):rm { recursive = true }
645+
Path:new("trg"):rm { recursive = true }
646+
end)
647+
648+
it_cross_plat("can copy a file with string destination", function()
649+
local p1 = Path:new "a_random_filename.rs"
650+
local p2 = Path:new "not_a_random_filename.rs"
651+
p1:touch()
652+
assert.is_true(p1:exists())
653+
654+
assert.no_error(function()
655+
p1:copy { destination = "not_a_random_filename.rs" }
656+
end)
657+
assert.is_true(p1:exists())
658+
assert.are.same(p1.filename, "a_random_filename.rs")
659+
assert.are.same(p2.filename, "not_a_random_filename.rs")
660+
end)
661+
662+
it_cross_plat("can copy a file with Path destination", function()
663+
local p1 = Path:new "a_random_filename.rs"
664+
local p2 = Path:new "not_a_random_filename.rs"
665+
p1:touch()
666+
assert.is_true(p1:exists())
667+
668+
assert.no_error(function()
669+
p1:copy { destination = p2 }
670+
end)
671+
assert.is_true(p1:exists())
672+
assert.is_true(p2:exists())
673+
assert.are.same(p1.filename, "a_random_filename.rs")
674+
assert.are.same(p2.filename, "not_a_random_filename.rs")
675+
end)
676+
677+
it_cross_plat("can copy to parent dir", function()
678+
local p = Path:new "some_random_filename.rs"
679+
p:touch()
680+
assert.is_true(p:exists())
681+
682+
assert.no_error(function()
683+
p:copy { destination = "../some_random_filename.rs" }
684+
end)
685+
assert.is_true(p:exists())
686+
end)
687+
688+
it_cross_plat("cannot copy an existing file if override false", function()
689+
local p1 = Path:new "a_random_filename.rs"
690+
local p2 = Path:new "not_a_random_filename.rs"
691+
p1:touch()
692+
p2:touch()
693+
assert.is_true(p1:exists())
694+
assert.is_true(p2:exists())
695+
696+
assert(pcall(p1.copy, p1, { destination = "not_a_random_filename.rs", override = false }))
697+
assert.no_error(function()
698+
p1:copy { destination = "not_a_random_filename.rs", override = false }
699+
end)
700+
assert.are.same(p1.filename, "a_random_filename.rs")
701+
assert.are.same(p2.filename, "not_a_random_filename.rs")
702+
end)
703+
704+
it_cross_plat("fails when copying folders non-recursively", function()
705+
local src_dir = Path:new "src"
706+
src_dir:mkdir()
707+
src_dir:joinpath("file1.lua"):touch()
708+
709+
local trg_dir = Path:new "trg"
710+
assert.has_error(function()
711+
src_dir:copy { destination = trg_dir, recursive = false }
712+
end)
713+
end)
714+
715+
describe("can copy directories recursively", function()
716+
local src_dir = Path:new "src"
717+
local trg_dir = Path:new "trg"
718+
719+
local files = { "file1", "file2", ".file3" }
720+
-- set up sub directory paths for creation and testing
721+
local sub_dirs = { "sub_dir1", "sub_dir1/sub_dir2" }
722+
local src_dirs = { src_dir }
723+
local trg_dirs = { trg_dir }
724+
-- {src, trg}_dirs is a table with all directory levels by {src, trg}
725+
for _, dir in ipairs(sub_dirs) do
726+
table.insert(src_dirs, src_dir:joinpath(dir))
727+
table.insert(trg_dirs, trg_dir:joinpath(dir))
728+
end
729+
730+
-- vim.tbl_flatten doesn't work here as copy doesn't return a list
731+
local function flatten(ret, t)
732+
for _, v in pairs(t) do
733+
if type(v) == "table" then
734+
flatten(ret, v)
735+
else
736+
table.insert(ret, v)
737+
end
738+
end
739+
end
740+
741+
before_each(function()
742+
-- generate {file}_{level}.lua on every directory level in src
743+
-- src
744+
-- ├── file1_1.lua
745+
-- ├── file2_1.lua
746+
-- ├── .file3_1.lua
747+
-- └── sub_dir1
748+
-- ├── file1_2.lua
749+
-- ├── file2_2.lua
750+
-- ├── .file3_2.lua
751+
-- └── sub_dir2
752+
-- ├── file1_3.lua
753+
-- ├── file2_3.lua
754+
-- └── .file3_3.lua
755+
756+
src_dir:mkdir()
757+
758+
for _, file in ipairs(files) do
759+
for level, dir in ipairs(src_dirs) do
760+
local p = dir:joinpath(file .. "_" .. level .. ".lua")
761+
p:touch { parents = true, exists_ok = true }
762+
assert.is_true(p:exists())
763+
end
764+
end
765+
end)
766+
767+
it_cross_plat("hidden=true, override=true", function()
768+
local success
769+
assert.no_error(function()
770+
success = src_dir:copy { destination = trg_dir, recursive = true, override = true, hidden = true }
771+
end)
772+
773+
assert.not_nil(success)
774+
assert.are.same(9, vim.tbl_count(success))
775+
for _, res in pairs(success) do
776+
assert.is_true(res.success)
777+
end
778+
end)
779+
780+
it_cross_plat("hidden=true, override=false", function()
781+
-- setup
782+
assert.no_error(function()
783+
src_dir:copy { destination = trg_dir, recursive = true, override = true, hidden = true }
784+
end)
785+
786+
local success
787+
assert.no_error(function()
788+
success = src_dir:copy { destination = trg_dir, recursive = true, override = false, hidden = true }
789+
end)
790+
791+
assert.not_nil(success)
792+
assert.are.same(9, vim.tbl_count(success))
793+
for _, res in pairs(success) do
794+
assert.is_false(res.success)
795+
assert.not_nil(res.err)
796+
assert.not_nil(res.err:match "^EEXIST:")
797+
end
798+
end)
799+
800+
it_cross_plat("hidden=false, override=true", function()
801+
local success
802+
assert.no_error(function()
803+
success = src_dir:copy { destination = trg_dir, recursive = true, override = true, hidden = false }
804+
end)
805+
806+
assert.not_nil(success)
807+
assert.are.same(6, vim.tbl_count(success))
808+
for _, res in pairs(success) do
809+
assert.is_true(res.success)
810+
end
811+
end)
812+
813+
it_cross_plat("hidden=false, override=false", function()
814+
-- setup
815+
assert.no_error(function()
816+
src_dir:copy { destination = trg_dir, recursive = true, override = true, hidden = true }
817+
end)
818+
819+
local success
820+
assert.no_error(function()
821+
success = src_dir:copy { destination = trg_dir, recursive = true, override = false, hidden = false }
822+
end)
823+
824+
assert.not_nil(success)
825+
assert.are.same(6, vim.tbl_count(success))
826+
for _, res in pairs(success) do
827+
assert.is_false(res.success)
828+
assert.not_nil(res.err)
829+
assert.not_nil(res.err:match "^EEXIST:")
830+
end
831+
end)
832+
end)
833+
end)
834+
638835
describe("parents", function()
639836
it_cross_plat("should extract the ancestors of the path", function()
640837
local p = Path:new(vim.fn.getcwd())

0 commit comments

Comments
 (0)