Skip to content

fix(core): rename instead of deleting when a patch only changes a file's case - #63655

Merged
rekram1-node merged 2 commits into
devfrom
patch-case-rename
Nov 13, 2026
Merged

rekram1-node merged 2 commits into
devfrom
patch-case-rename

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Issue for this PR

Closes #63126

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

A patch that moves a file writes the new name, then removes the old one. On macOS and Windows file names ignore case by default, so a move that only changes case (same.txt → Same.txt) wrote the file in place and then deleted it, and the tool failed with "file does not exist" on an empty directory.

before: write Same.txt (= same.txt) → remove same.txt → file gone
after:  write same.txt → rename same.txt to Same.txt

A move now takes the rename path only when the two paths match ignoring case, the source is a regular file, and the new name already exists, which on a case-insensitive filesystem means it's the same file. Everything else keeps the write-and-remove path, so on Linux a move into a directory that differs only in case still creates it, and a symlink's target is left alone. If a different file with the new name exists on Linux, the rename ends the same way write-and-remove does: the new name holds the new contents and the old name is gone.

How did you verify your code works?

  • New test: same.txt moved to Same.txt leaves only Same.txt with the new contents. It fails on dev on macOS (the directory ends up empty).
  • New test: Foo/a.txt moved to foo/a.txt succeeds. On Linux, where foo/ doesn't exist, it failed with the first version of this PR.
  • bun run script/test.ts test/tool-patch.test.ts in packages/core (44 pass), bun run check, and typecheck:tsc in packages/core.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@rekram1-node
rekram1-node enabled auto-merge (squash) November 13, 2026 03:00

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a case-insensitive filesystem the patch no longer deletes the file, but the new check matches too broadly and breaks two moves on Linux that work on dev today. Changes requested.

How I checked it: I ran the new test on a vfat (case-insensitive) mount. On the base it fails and the directory ends up empty, which is the #63126 data loss. With this PR the file survives, but it keeps the name same.txt. As far as I know, Linux's vfat driver ignores case-only renames, so this probably says nothing about macOS or Windows. I couldn't check those. I also compared the two moves below against the base on a normal Linux tmp dir. tool-patch.test.ts passes (43 tests) and typecheck:tsc is clean.

Public API: unchanged.

Concern (inline): recased matches any two paths that are equal ignoring case. That includes moves between directories that differ only in case, and moves of symlinks. On Linux both now behave differently from dev: one fails partway through, the other writes through the link into its target.

Comment thread packages/core/src/tool/plugin/patch.ts Outdated
const target = moveTarget ?? change.target
// A move that only changes case is the same file on macOS and Windows, where writing the new name
// and removing the old one deletes it, so it is written in place and renamed.
const recased = moveTarget?.absolute.toLowerCase() === change.target.absolute.toLowerCase()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches any two paths that are equal ignoring case, not only ones that are the same file. On a case-sensitive filesystem (Linux), this breaks two moves that work on dev. I compared both against the base:

  • Foo/a.txt → foo/a.txt when foo/ doesn't exist: dev creates foo/a.txt. Here the content is written into Foo/a.txt in place, then move fails with ENOENT (it doesn't create parent directories). The tool errors with the old file already changed.
  • Symlink link.txt → Link.txt: dev leaves the link's target alone and writes a regular Link.txt. Here the new content is written through the link into its target, and the link is renamed. That's what the "moves a symlink without deleting its target" test guards against for normal moves.

One option: take the rename path only when the two paths really point to the same file. For example, require that the move target already exists, which only happens for a case-only rename on a case-insensitive filesystem, and that the source isn't a symlink. Everything else would keep the write-and-remove path.

@rekram1-node
rekram1-node merged commit 2f8a867 into dev Nov 13, 2026
12 checks passed
@rekram1-node
rekram1-node deleted the patch-case-rename branch November 13, 2026 03:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

patch: case-only rename deletes the file on case-insensitive filesystems

1 participant