Repository navigation
Description
What's wrong
The fix for #292 (commit a35b467, PR #297) changed SchemaFile.TrySave from File.WriteAllText(target) to writing <target>.tmp-<guid> and then swapping it in with File.Replace(temporaryPath, targetPath, null):
Schema/Schema.Editor/SchemaFile.cs
Lines 145 to 169 in 2c0a7d8
| private static void WriteAtomically(string targetPath, string content, Action<string, string> writeFile) | |
| { | |
| string temporaryPath = $"{targetPath}.tmp-{Guid.NewGuid():N}"; | |
| try | |
| { | |
| writeFile(temporaryPath, content); | |
| if (File.Exists(targetPath)) | |
| { | |
| File.Replace(temporaryPath, targetPath, destinationBackupFileName: null); | |
| } | |
| else | |
| { | |
| File.Move(temporaryPath, targetPath); | |
| } | |
| } | |
| finally | |
| { | |
| if (File.Exists(temporaryPath)) | |
| { | |
| File.Delete(temporaryPath); | |
| } | |
| } | |
| } |
On Linux and macOS, File.Replace is a rename(2) onto the target path. That replaces the directory entry, so:
- A symlinked schema is cut loose from what it links to. If
game.schema.jsonis a symlink toshared/game.schema.json(a schema shared between projects or kept in a sibling checkout), saving from the editor deletes the link and puts a regular file in its place. The real file the link pointed at keeps the old content, and every other project that uses it never sees the edit. Nothing reports this: the save succeeds, the editor title goes clean, and the nextgit statusin the shared repository shows no change.File.WriteAllText, which the editor used until two days ago, followed the link and wrote the shared file. - The file's mode is reset to the process default. The temporary file is created with the umask's mode, and it becomes the target, so a schema that was
rw-rw-r--(group-writable, for a team share) isrw-r--r--after one save from the editor.
Repro
A scratch console app (net10.0, Linux) that does exactly what WriteAtomically does:
var real = Path.Combine(d, "shared", "game.schema.json");
File.WriteAllText(real, "OLD");
File.SetUnixFileMode(real, UserRead|UserWrite|GroupRead|GroupWrite|OtherRead);
var link = Path.Combine(d, "game.schema.json");
File.CreateSymbolicLink(link, real);
string tmp = $"{link}.tmp-{Guid.NewGuid():N}";
File.WriteAllText(tmp, "NEW");
File.Replace(tmp, link, null); // SchemaFile.WriteAtomically
Console.WriteLine($"link still a symlink: {new FileInfo(link).LinkTarget != null}");
Console.WriteLine($"shared file content: {File.ReadAllText(real)}");
Console.WriteLine($"link path content: {File.ReadAllText(link)}");
// and the same on a plain group-writable file:
Console.WriteLine($"mode before: ...; after: {File.GetUnixFileMode(real2)}");Output:
link still a symlink: False
shared file content: OLD
link path content: NEW
mode before: OtherRead, GroupWrite, GroupRead, UserWrite, UserRead; after: OtherRead, GroupRead, UserWrite, UserRead
Verified on Linux only. The roadmap makes the editor Windows-first but "defensively cross-platform", and Schema.Editor.Test exists because saving differs by operating system, which is why this is filed as a bug and not a platform gap. I did not test Windows ReplaceFile against a symbolic link.
Suggested fix
- Before the swap, resolve the target through any links (
new FileInfo(path).ResolveLinkTarget(returnFinalTarget: true)). Write the temporary file beside that final target and replace that, so the link stays a link and the shared file is the one updated. - When replacing an existing file on Unix, copy its mode onto the temporary file (
File.SetUnixFileMode(tmp, File.GetUnixFileMode(target))) before the swap.
Acceptance criteria
- Saving through a symlink leaves the symlink in place and updates the file it points to. This is a test in
Schema.Editor.Test/SchemaFileTests.cs, skipped where symlinks can't be created. - Saving over an existing file keeps its Unix permission bits.
- The guarantees from A failed save in Schema.Editor leaves the existing .schema.json truncated, because the file is overwritten in place #292 still hold: a failed write leaves the previous content intact and no temporary file behind.
Activity
matt-edmondson commentedon Oct 20, 2026
Triage
Schema.Editor/SchemaFile.cs, inTrySave/WriteAtomically, with tests inSchema.Editor.Test/SchemaFileTests.cs. Owner: @matt-edmondsonWriteAtomically. Keep the close button working after an abandoned save from the close prompt #313, about the close prompt after an abandoned save, is in the same save path but is a different fix, so expect a merge conflict if both land close together.File.Replace. Keep the A failed save in Schema.Editor leaves the existing .schema.json truncated, because the file is overwritten in place #292 tests for the case where the write fails.Generated by Claude Code