You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 67eb227
Browse filesBrowse the repository at this point in the historyBrowse files
Harden Move/Copy destination handling and make link replacement rollback-safe
Adds a shared final-path classification for standard Move/Copy: type
conflicts (folder onto file, file onto folder) are blocked outright instead
of merged, destinations that are reparse points are refused (/XJ does not
make a linked destination argument safe), and the classification is
re-checked immediately before robocopy runs so drift between review and
execution stops the job instead of running it.
Move + Symlink's existing-link replacement is now a single rollback-safe
transaction: the old link is removed only immediately before the new one is
created, and a failed creation restores the original link automatically,
reporting failure rather than success either way.
Also fixes a .gitignore ordering bug that silently re-ignored the tracked
.env template files, and updates CI to run the full regression suite under
both Windows PowerShell 5.1 and PowerShell 7+ on every push and pull request.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+6-2Lines changed: 6 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,14 +6,18 @@
6
6
7
7
- Fixed Move and Copy for folder sources so the selected folder is transferred as a folder and keeps its own name at the destination, instead of flattening its contents into the selected destination path. A destination that already ends with the source folder's name is reused instead of doubling the name (for example, no more `...\Docs\Docs`). Single-file Move and Copy are unaffected and keep the original file name.
8
8
- Fixed `Invoke-RobocopyCommand` and `Invoke-CmdDeleteCommand` so they return exactly one integer exit code. Native command output was previously captured into the same pipeline as the exit code, which could make a log entry read as `exitCode=System.Object[]` instead of a number such as `3`.
9
-
- Fixed Move + Symlink so an existing symbolic link or junction at the original path is no longer removed during preview/validation. It is now removed only after the final confirmation, immediately before the replacement link is created, so cancelling leaves the existing link untouched.
9
+
- Fixed Move + Symlink so an existing symbolic link or junction at the original path is no longer removed during preview/validation. It is now removed only immediately before the replacement link is created, as one rollback-safe transaction: if creating the new link fails, RoboSy automatically restores the original link and reports the job as failed, never as successful. If the restore itself cannot complete, RoboSy reports a critical error with the original link's target and kind plus a manual recovery command. Cancelling with `n`, `0`, `exit`, or `quit`leaves the existing link completely untouched.
10
10
- Updated the README to describe the existing Move + Symlink behavior for a same-name existing target folder (merge instead of stop), matching the current code instead of the previously stale description.
11
+
- Fixed the `.gitignore` Hook Maker block so its `!/.env.example`-style exceptions come after the broad `/.env.*` rule instead of before it; the exceptions were previously overridden by the later broad rule and had no effect.
11
12
12
13
### Added
13
14
14
15
- Move and Copy now show the effective final destination path (and, on collision, what already exists there) in the review screen, command preview, breadcrumbs, and logs before asking for confirmation.
15
16
- Move and Copy now block drive roots, share roots, and protected system/profile roots as sources, matching the existing Fast Delete guard.
16
-
- Added `tests/RoboSy.Tests.ps1`, a non-interactive regression test suite covering destination resolution, robocopy argument building, native-command return values, and symlink replacement safety.
17
+
- Move and Copy now classify the final destination path before running: a type conflict (a folder onto an existing file, or a file onto an existing folder) is blocked outright instead of being merged or overwritten, and a destination that is itself a symbolic link, junction, or other reparse point is refused, since `/XJ` only excludes junctions encountered while recursing and does not make a linked destination argument safe.
18
+
- Move and Copy now re-check the destination classification immediately before running, using the same command computed at review time. Any drift since the review (the final path changed type, appeared, disappeared, or became a reparse point) stops the job before `robocopy` runs.
19
+
- Added `tests/TestHelpers.ps1`, `tests/RoboSy.LinkSafety.Tests.ps1`, and `tests/RoboSy.Classification.Tests.ps1` alongside `tests/RoboSy.Tests.ps1`, covering the rollback-safe link replacement transaction, final-path classification, type conflicts, reparse-point hardening, and execution-time revalidation, including real interactive cancel/confirm scenarios driven through piped input.
20
+
-`.github/workflows/lint.yml` now runs the full regression test suite under both Windows PowerShell 5.1 and PowerShell 7+ on every push and pull request, in addition to the existing parse check and PSScriptAnalyzer run.
Copy file name to clipboardExpand all lines: README.md
+17-6Lines changed: 17 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,6 +32,9 @@ It is designed for users who want a simple Windows terminal workflow without man
32
32
- Refuse symbolic links, junctions, and other reparse points as Move/Copy sources to avoid accidentally moving the real target's contents.
33
33
- Refuse drive roots, share roots, and protected system/profile roots as Move/Copy sources.
34
34
- Transfer a selected folder as a folder, preserving its name at the destination, instead of flattening its contents into the destination root.
35
+
- Block a Move/Copy when the resolved destination is a type conflict (a folder onto an existing file, or a file onto an existing folder) instead of merging or overwriting the wrong thing.
36
+
- Refuse a Move/Copy destination that is itself a symbolic link, junction, or other reparse point, and re-check the destination immediately before running so a change between review and execution stops the job instead of running it.
37
+
- Roll back automatically if replacing an existing Move + Symlink link fails partway through, so a failed replacement never leaves you without the original link.
35
38
- Print total elapsed time after each operation.
36
39
- Write daily log files next to the script, with fallback to `%LOCALAPPDATA%\RoboSy\logs`.
37
40
@@ -52,6 +55,10 @@ RoboSy blocks drive roots, share roots, and protected root paths for destructive
52
55
53
56
Move and Copy refuse source paths that are symbolic links, junctions, or other reparse points. Choose the real target path directly, or use Move + Symlink when you want to manage a link.
54
57
58
+
Move and Copy also refuse a destination that is a symbolic link, junction, or other reparse point, and refuse a type conflict (folder onto an existing file, or file onto an existing folder) instead of merging or overwriting the wrong thing. The destination is checked again immediately before the job runs, so a change between the review screen and confirmation stops the job instead of running it.
59
+
60
+
If replacing an existing Move + Symlink link fails partway through, RoboSy restores the original link automatically and reports the job as failed, never as successful.
61
+
55
62
## Installation
56
63
57
64
Clone the repository:
@@ -115,7 +122,7 @@ Use this when you want RoboSy to transfer data and remove the original copy afte
115
122
116
123
For a folder source, RoboSy transfers the folder itself: entering destination `F:\B` for source `E:\A\Docs` moves it to `F:\B\Docs`, not into `F:\B` directly. A destination that already ends with the source folder's name (for example `F:\B\Docs`) is used as-is instead of doubling the name. A single file keeps its original file name at the destination.
117
124
118
-
If the resolved final path already exists and holds other items, RoboSy shows what it found and asks for a separate confirmation before merging into it or overwriting a same-named file.
125
+
If the resolved final path already exists and holds other items, RoboSy shows what it found and asks for a separate confirmation before merging into it or overwriting a same-named file. A type conflict at the final path (a folder onto an existing file, or a file onto an existing folder) is blocked outright; `robocopy` is never invoked for it.
119
126
120
127
If `robocopy` leaves an empty source folder behind after a directory move, RoboSy attempts to remove that empty source folder.
121
128
@@ -155,6 +162,7 @@ Behavior:
155
162
- If the original path is missing and the target path exists, RoboSy only creates the link.
156
163
- If both paths already exist, RoboSy stops without overwriting either path, unless the target is an existing folder whose name already matches the source folder name; in that case RoboSy moves the source's contents into that existing folder before creating the link.
157
164
- If the original path is already a symbolic link or junction, RoboSy leaves it untouched until you confirm the replacement, then removes only the link entry (never its target) immediately before creating the new link.
165
+
- If creating the replacement link fails after the old link was removed, RoboSy immediately restores the original link and reports the job as failed. If the restore itself cannot complete, RoboSy reports a critical error with the original link's target and a manual recovery command; the old target is never deleted or modified either way.
158
166
- If directory symlinks are unavailable, RoboSy tries a junction fallback.
159
167
160
168
## Marker File
@@ -199,7 +207,10 @@ Runtime logs are ignored by Git and should not be published.
199
207
| File | Purpose |
200
208
| --- | --- |
201
209
|`RoboSy.ps1`| Main interactive PowerShell script. |
202
-
|`tests/RoboSy.Tests.ps1`| Non-interactive regression tests for path resolution and native-command wrappers. |
210
+
|`tests/TestHelpers.ps1`| Shared test assertions, sandbox helpers, and the interactive end-to-end test harness. |
211
+
|`tests/RoboSy.Tests.ps1`| Regression tests for destination resolution and native-command wrappers. |
212
+
|`tests/RoboSy.LinkSafety.Tests.ps1`| Regression tests for the rollback-safe Move + Symlink replacement transaction. |
213
+
|`tests/RoboSy.Classification.Tests.ps1`| Regression tests for final-path classification, type conflicts, reparse-point hardening, and execution-time revalidation. |
203
214
|`RoboSy.cmd`| Normal launcher. |
204
215
|`RoboSy Admin.cmd`| Administrator launcher. |
205
216
|`Install-RoboSyPath.ps1`| Adds RoboSy to the user `PATH` and installs the command shim. |
@@ -237,7 +248,7 @@ These local paths are intentionally ignored by Git:
237
248
238
249
## Development
239
250
240
-
RoboSy is linted with [PSScriptAnalyzer](https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/overview) using the rules in `PSScriptAnalyzerSettings.psd1`. The same checks run in GitHub Actions (`.github/workflows/lint.yml`).
251
+
RoboSy is linted with [PSScriptAnalyzer](https://learn.microsoft.com/powershell/utility-modules/psscriptanalyzer/overview) using the rules in `PSScriptAnalyzerSettings.psd1`, and its regression tests run under both Windows PowerShell 5.1 and PowerShell 7+. The same checks run in GitHub Actions (`.github/workflows/lint.yml`) on every push and pull request that touches a `.ps1` file.
241
252
242
253
Run the checks locally before pushing:
243
254
@@ -251,11 +262,11 @@ A clean run reports no issues.
251
262
Run the regression tests locally with either PowerShell version:
Tests run entirely inside disposable temporary directories and clean up after themselves.
269
+
Each test file prints its own passed/failed/skipped counts and exits non-zero on any failure. Tests run entirely inside disposable temporary directories and clean up after themselves; the interactive end-to-end scenarios in `RoboSy.LinkSafety.Tests.ps1` and `RoboSy.Classification.Tests.ps1` drive a disposable copy of `RoboSy.ps1` through piped input, so its own log directory never touches the real repository.
0 commit comments