Skip to content

Commit 0768000

Browse files
committed
WIP
1 parent 9ac3e95 commit 0768000

File tree

1 file changed

+56
-13
lines changed

1 file changed

+56
-13
lines changed

lua/plenary/path.lua

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -515,30 +515,73 @@ function Path:rmdir()
515515
uv.fs_rmdir(self:absolute())
516516
end
517517

518+
---Rename this file or directory to the given target, and return a new Path instance pointing to target. On Unix, if target exists and is a file, it will be replaced silently if the user has permission. On Windows, if target exists, FileExistsError will be raised. target can be either a string or another path object:
519+
---@generic T: Path
520+
---@param opts { new_name: Path|string }
521+
---@return T
518522
function Path:rename(opts)
523+
-- TODO: For reference, Python's `Path.rename()` actually says/does this:
524+
--
525+
-- > On Unix, if target exists and is a file, it will be replaced silently
526+
-- > if the user has permission.
527+
-- >
528+
-- > On Windows, if target exists, FileExistsError will be raised. target
529+
-- > can be either a string or another path object.
530+
--
531+
-- The behavior here may differ, as an error will be thrown regardless.
532+
533+
local self_lstat, new_lstat, status, errmsg
519534
opts = opts or {}
520-
if not opts.new_name or opts.new_name == "" then
521-
error "Please provide the new name!"
522-
end
535+
assert(opts.new_name and opts.new_name ~= "", "Please provide the new name!")
536+
self_lstat, errmsg = uv.fs_lstat(self.filename)
537+
538+
-- Cannot rename a non-existing path (lstat is needed here, `Path:exists()`
539+
-- uses stat)
540+
assert(self_lstat, ("%s: %s"):format(errmsg, self.filename))
523541

542+
-- BUG
524543
-- handles `.`, `..`, `./`, and `../`
525544
if opts.new_name:match "^%.%.?/?\\?.+" then
526545
opts.new_name = {
527546
uv.fs_realpath(opts.new_name:sub(1, 3)),
528-
opts.new_name:sub(4, #opts.new_name),
547+
opts.new_name:sub(4),
529548
}
530549
end
531550

532551
local new_path = Path:new(opts.new_name)
533-
534-
if new_path:exists() then
535-
error "File or directory already exists!"
536-
end
537-
538-
local status = uv.fs_rename(self:absolute(), new_path:absolute())
539-
self.filename = new_path.filename
540-
541-
return status
552+
new_lstat, errmsg = uv.fs_lstat(new_path.filename)
553+
554+
-- BUG: exists() uses stat, not lstat. `new_name` could be a bad symlink, and
555+
-- then will incorrectly think that it doesn't exist (rename(2) does not
556+
-- resolve the final path component if it's a symlink!)
557+
-- if new_lstat then
558+
559+
-- This allows changing only case (e.g. fname -> Fname) on case-insensitive
560+
-- file systems, otherwise throwing if `new_name` exists.
561+
--
562+
-- NOTE: to elaborate, `uv.fs_rename()` wont/shouldn't do anything if old
563+
-- and new both exist and are both hard links to the same file (inode),
564+
-- however, it appears to still allow you to change the case of a filename
565+
-- on case-insensitive file systems (e.g. if `new_name` doesn't _actually_
566+
-- exist as a separate file but would otherwise appear to via an lstat call;
567+
-- if it does actually exist (in which case the fs must be case-sensitive)
568+
-- idk 100% what happens b/c it needs to be tested on a case-sensitive fs,
569+
-- but it should then simply be a successful no-op according to rename(2)
570+
-- docs, at least on Linux anyway)
571+
assert(not new_lstat or (self_lstat.ino == new_lstat.ino), "File or directory already exists!")
572+
573+
status, errmsg = uv.fs_rename(self:absolute(), new_path:absolute())
574+
assert(status, ("%s: Rename failed!"):format(errmsg))
575+
576+
-- NOTE: `uv.fs_rename()` _can_ return success even if no rename actually
577+
-- occurred (see rename(2)), and this is not an error...we're not changing
578+
-- `self.filename` if it didn't.
579+
if not uv.fs_lstat(self.filename) then
580+
self.filename = new_path.filename
581+
end
582+
583+
-- TODO: Python returns a brand new instance here, should we do the same?
584+
return self
542585
end
543586

544587
--- Copy files or folders with defaults akin to GNU's `cp`.

0 commit comments

Comments
 (0)