|
3 | 3 | // |
4 | 4 | // The adapter is a thin façade. Operations that map one-for-one |
5 | 5 | // (writeFile, readdir, mkdir, rm, chmod, symlink, readlink, stat, |
6 | | -// lstat) forward directly. Operations the stub doesn't expose — |
7 | | -// appendFile, cp, mv, exists — synthesize from the available |
| 6 | +// lstat, rename) forward directly. Operations the stub doesn't |
| 7 | +// expose — appendFile, cp — synthesize from the available |
8 | 8 | // primitives. Hard links and utimes aren't supported: link throws |
9 | 9 | // ENOSYS so a script that depends on them fails loudly; utimes |
10 | 10 | // is a documented no-op because the store has no atime column. |
@@ -50,6 +50,7 @@ export interface WorkspaceFs { |
50 | 50 | rm(path: string, options?: RmOptions): Promise<void>; |
51 | 51 | chmod(path: string, mode: number): Promise<void>; |
52 | 52 | symlink(target: string, path: string): Promise<void>; |
| 53 | + rename(oldPath: string, newPath: string): Promise<void>; |
53 | 54 | } |
54 | 55 |
|
55 | 56 | // Matches the subset of just-bash's IFileSystem the adapter |
@@ -213,10 +214,19 @@ export class WorkspaceFsAdapter { |
213 | 214 | } |
214 | 215 |
|
215 | 216 | async mv(src: string, dest: string): Promise<void> { |
216 | | - // The store doesn't have a native rename today, so model mv as |
217 | | - // copy+delete. POSIX mv is atomic when src and dest live on |
218 | | - // the same filesystem; this approach isn't, but it matches |
219 | | - // what just-bash's other adapters do. |
| 217 | + // The store renames in one transaction, so an interrupted move can |
| 218 | + // no longer leave the bytes at both paths or a directory half |
| 219 | + // copied. A destination that rename refuses to replace — a |
| 220 | + // non-empty directory, or a directory and a non-directory in |
| 221 | + // either order — still falls back to copy-then-delete, which is |
| 222 | + // what the shell's own `mv` expects when it merges a tree. |
| 223 | + try { |
| 224 | + await this.#fs.rename(src, dest); |
| 225 | + return; |
| 226 | + } catch (err) { |
| 227 | + const code = (err as { code?: string }).code; |
| 228 | + if (code !== "ENOTEMPTY" && code !== "EISDIR" && code !== "ENOTDIR") throw err; |
| 229 | + } |
220 | 230 | await this.cp(src, dest, { recursive: true }); |
221 | 231 | await this.#fs.rm(src, { recursive: true }); |
222 | 232 | } |
|
0 commit comments