Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add dialog for existing file names during copy/paste - #708

Merged
l0gicgate merged 4 commits into
lgse:mainfrom
fadilasif:feat/add-dialog-for-existing-file-names
Sep 11, 2026
Merged

feat: add dialog for existing file names during copy/paste#708
l0gicgate merged 4 commits into
lgse:mainfrom
fadilasif:feat/add-dialog-for-existing-file-names

Conversation

@fadilasif

@fadilasif fadilasif commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

Refine the existing copy/paste conflict dialog: show Skip whenever cancelling would discard other accepted items, and show Apply to All in the action row only when further conflicts remain. Keep same-folder paste and Duplicate as silent numbered copies. Explicit self-replacement is a safe no-op.

Visual evidence

Original author attachment (before the follow-up restored silent same-folder duplication):

Screenshot_proof

How to test

  1. Copy several files to another directory where only one name already exists. Choose Skip; verify the other files are copied and the existing file remains unchanged.
  2. Paste two conflicting files. Choose Keep Both for the first, then Skip for the last; verify the first numbered copy is preserved. Apply to All should appear only while further conflicts remain.
  3. Repeat with Replace, Cancel, Escape, and the dialog close button. Replace overwrites only the chosen destination; cancellation abandons the entire pending batch.
  4. Copy/paste into the source directory or use Duplicate. Verify a numbered copy is created without a conflict dialog.
  5. Undo a multi-file move after recreating one original name. Skip that collision; verify the non-conflicting item returns and the new occupant remains intact.

Expected result: Conflict choices preserve accepted work; cancellation changes nothing; same-folder duplicates remain non-destructive and do not prompt.

Related issue

Related to #395 and #270 (already closed); this is a follow-up, not a new resolution of those issues.

@wmfeht

wmfeht commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Merge Blocker

Hiding Skip whenever only one name collision remains makes Cancel the only way to decline that item. Cancel still aborts the whole transfer, including already-accepted non-conflicting files and earlier conflict choices.

So a mixed paste (several files, one name conflict) or the last remaining conflict after Keep Both cannot skip the conflict without dropping the rest of the batch. New tests currently encode the missing Skip as intended.

Fix: keep Skip visible whenever Cancel would drop already-accepted work (!accepted.is_empty() || !remaining.is_empty()). Keep Apply to All tied to remaining conflicts only.


Review: lgse/strata PR #708

Title: feat: add dialog for existing file names during copy/paste
Author: fadilasif
Head: 890781a934f9f3f79d4996256cb28a041acb7cff
Base: main (a4dcfaae66be4a385351b6925235917b136e8bc2)
Diff: 7 files, +546 / −13, 1 commit
Mergeability: behind main
CI at review time: Metadata policy success; [code]smith skipped; no format/clippy/Rust/E2E checks yet


1. What the change does (vs PR claims)

The PR claims: show a conflict dialog on copy/paste (and Duplicate) when the destination already has that name, instead of silently creating name (1).ext. Actions: Replace, Keep Both, Skip where applicable, Cancel, Apply to All.

That is accurate for the same-folder path, and it is a small production change wrapped in a lot of tests:

  1. Collision detection (transfer_has_collision in src/ui/browser/transfer.rs): stop treating source == destination/name as “not a collision.” Same-directory copy/paste and Ctrl+D now hit confirm_replace_conflict.
  2. Adapter safety (LocalOperationProvider::paste in src/adapters/local_operations.rs): same-directory Replace is a no-op so a file is not copied onto itself. Keep Both still uses duplicate_target.
  3. Dialog chrome: Skip is shown only when has_more_conflicts; Apply to All moves from the body into the action bar and is relabeled "Apply to All".

Cross-folder Keep Both / Replace / Cancel already existed on main (closed #395). This PR is not “add a conflict dialog from scratch”; it routes same-folder duplicates through that dialog and tightens Skip visibility.


2. BLOCKER — Skip hidden when Cancel is not equivalent

Severity: correctness / data-loss-of-the-paste (not disk corruption)
Where: ViewState::confirm_replace_conflict and resolve_transfer_collisions in src/ui/browser/transfer.rs

has_more_conflicts is !collisions.is_empty() after popping the current name. Skip and Apply to All both use that flag:

        let apply_all = form_check_button("Apply to All");
        apply_all.set_visible(has_more_conflicts);
        layout.actions.prepend(&apply_all);
        let skip = gtk::Button::with_label("Skip");
        skip.add_css_class("action-dialog-cancel");
        skip.set_visible(has_more_conflicts);

Non-conflicting items are already in accepted before the first dialog:

        for source in sources {
            if transfer_has_collision(&source, &destination) {
                collisions.push(source);
            } else {
                accepted.push(PasteItem {
                    source,
                    conflict: TransferConflict::FailIfExists,
                });
            }
        }
        self.resolve_transfer_collisions(destination, collisions, accepted, move_sources);

Cancel / Escape / X dismiss the modal and do not call on_choice (comment on confirm_replace_conflict: “Cancelling abandons the whole operation”). So:

Situation Skip visible? What the user can do
1 file, it conflicts No Cancel ≡ Skip. Fine.
4 files, 1 name conflict No Cannot skip the conflict and still paste the other 3. Cancel drops all 4.
2 conflicts; user Keep Both on the first No on the last Cannot skip the last name without also throwing away the first Keep Both.

Issue #391 already defined this: X/Cancel/Escape abandon the whole batch, including earlier choices. Skip is the “continue without this item” action. This PR removes Skip exactly when that distinction matters.

The new tests assert the regression:

  • skip_is_hidden_when_only_one_item_conflicts (4 sources, 1 collision, Skip must be absent)
  • E2E test_pasting_a_duplicate_name_asks_before_replacing used to click Skip; it now asserts Skip is missing and clicks Replace instead

Same has_more_conflicts is passed for undo-move collisions (resolve_undo_collisions). Undoing a multi-item move where only one original name is occupied now has the same Cancel-vs-Skip trap.

Visual proof (mixed paste, 3 files, 1 conflict)

Copied conflict.txt, extra-a.txt, extra-b.txt from mixed-src into mixed-dest (already had conflict.txt). Dialog: Cancel / Keep Both / Replace. No Skip, no Apply to All. Replace is the danger default. Body still says replacing will overwrite.

Mixed paste conflict dialog with no Skip button

Clicked Cancel. Destination still only contains original conflict.txt (old-conflict). The two non-conflicting files were not pasted.

Fix: keep Skip visible whenever Cancel would drop work (!accepted.is_empty() || !remaining.is_empty()). Keep Apply to All tied to remaining conflicts only.


3. High — same-folder / Duplicate uses a danger Replace dialog whose Replace is a no-op

Where: transfer_has_collision (now reports same-folder as a collision) + paste no-op for is_duplicate && ReplaceExisting + replace.grab_focus() + copy that always says overwrite.

Same-folder paste and Ctrl+D now open the same “File already exists” dialog as a cross-folder overwrite. Replace is focused, danger-styled, and the body says “Replacing it will overwrite its contents.” For a same-directory copy, Replace does not overwrite; it no-ops (good for not deleting the file, bad as default).

Enter / clicking Replace on Duplicate or same-folder paste does nothing. Keep Both is the actual duplicate action.

This fights closed #270 (“Prompting a replacement dialog is destructive and undesirable for duplicate workflows”) even though it matches this PR’s stated test plan.

Same-folder paste dialog claiming Replace will overwrite

Keep Both does create photo (1).jpg and leaves the original (confirmed on disk). The listing in that clip did not show the new name before the session screensaver; the copy itself succeeded.

Product choice William should make: either (a) Duplicate / same-folder copy skip the dialog and Keep Both automatically, or (b) keep the prompt but default Keep Both, drop Danger/overwrite copy for self-collisions, and do not focus Replace.

The adapter no-op itself is the right safety net if Replace stays offered.


4. Non-blocking

Apply to All in the action bar. Label shortened from “Apply this choice to all remaining conflicts”; widget prepended to .action-dialog-actions. Skip still appears for true multi-conflict:

Multi-conflict dialog with Apply to All and Skip

Layout is usable (checkbox left, buttons right). The old body sentence was clearer. E2E test_keep_both_applies_to_all_collisions_in_a_mixed_paste was updated for the new accessible name.

Drive-by naming tests. Hidden-file / tar.gz suffix tests in local_operations/tests.rs do not change parse_copy_suffix. They document existing backup.tar (1).gz behavior. Harmless, not this feature.

Process. No Closes # / Related issue. Closest prior work is closed #395 (unify same-folder vs cross-folder; Keep Both already on main). PR body duplicates “How to test.” Branch is behind main.

Theme test. conflict_dialog_verifies_theme_following only compares resolved theme_surface after switching themes. The dialog already used @theme_*. It does not cover Apply to All / Skip / Replace states.


5. Security / safety

No high-confidence vulnerability.

  • Overwrite is an explicit Replace on a local file manager, not an unauthenticated path.
  • Same-directory Replace is a no-op; it does not copy a file onto itself or trash the original.
  • Copy undo is keyed off created_location; a same-folder Replace no-op emits created_location: None, and push_pending_undo skips empty Copy entries, so Ctrl+Z will not trash the original.
  • Collision uses GIO query_exists, not the in-memory listing, so a stale list does not by itself skip the dialog.
  • Remaining TOCTOU (file appears after FailIfExists was chosen; sequential dialogs use a snapshot collision list) is pre-existing.

6. Focus-area notes

Overwrite vs Keep Both vs Cancel. Cross-folder Replace still overwrites; Keep Both still uses duplicate_target; Cancel still means abort-all. Same-folder Replace is a silent success no-op with overwrite copy. Skip is no longer available in the mixed/last-conflict cases above.

Same-folder vs cross-folder. Policy is now “always prompt,” which is consistent in the sense that both paths use one dialog, inconsistent in Replace semantics (overwrite vs no-op) and with Duplicate.

Multi-file paste. Non-conflicts are queued first, then collisions one-by-one. Apply to All Keep Both still works (existing E2E). Skip-on-last / Skip-on-only-collision is the hole.

Listing refresh. Detection is disk query_exists, not the GTK model. Unique names for Keep Both are chosen at paste time, so a name (1) that appears while the dialog is open should bump to (2). No new TOCTOU in that path. Sequential dialogs still do not re-probe remaining names.

Consistency with existing Keep Both UX. Keep Both still hidden for moves; still numbered stem (n).ext; still not offered on undo. New inconsistency is Skip visibility vs that older dialog.


7. Test coverage gaps vs claimed behavior

Claimed: Skip / Apply to All / Cancel “behave correctly” for multiple conflicts.

Missing / inverted:

  • No test that mixed paste + Skip (or Cancel-equivalent) still pastes non-conflicting items. The new tests require Skip to be absent in that situation.
  • No test that after Keep Both on conflict 1, Skip on conflict 2 still performs the first Keep Both.
  • No undo-move test for Skip hidden on a single remaining original-name collision.
  • No GTK/E2E test that same-folder Replace is a no-op and that Enter hits that no-op (adapter unit test only).
  • No test that Duplicate (Ctrl+D) defaults to Keep Both rather than Replace.
  • Theme test does not cover the moved Apply to All control.

What they did add is useful for the happy path: same-folder dialog appears; Skip shown when two collisions exist; same-folder Replace no-op and Keep Both at the adapter; E2E same-folder copy now clicks Keep Both.


8. What looks solid

  • The collision-hook change (source.equal(&target) no longer excluded) is the right place to fold same-folder copy into the existing dialog.
  • Self-replace no-op is the correct filesystem answer if Replace stays on that path.
  • Cross-folder Keep Both / Replace / move-without-Keep-Both are left intact.
  • Same-folder cut/paste is still a no-op via transfer_is_noop (parent == destination).
  • Unique names still go through duplicate_target at paste time.
  • Empty same-folder Replace does not poison undo.
  • Production diff is small and localized; most of the PR is tests.

9. Recommendation on landing

Do not land until Skip is restored whenever Cancel would drop already-accepted items (mixed paste and last remaining conflict, including undo collisions), and the tests that currently require Skip to be hidden in those cases are inverted.

Treat same-folder / Duplicate defaulting to danger Replace (no-op, “will overwrite”) as a product decision: keep a prompt only if Keep Both is the default and the copy matches self-collision. Otherwise Duplicate should stay a silent numbered copy.

After that, rebase onto main and wait for the full CI suite (not just Metadata policy).

Suggested GitHub review when you post one: Request changes, with the Skip/Cancel mixed-paste issue as the required fix.

@fadilasif

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in latest commit

Restored Skip whenever Cancel would discard already accepted work, including mixed pastes and the final remaining conflict.
Kept Apply to All visible only when additional conflicts remain.
Updated same-directory copy/Duplicate handling to avoid the misleading Replace flow.
Updated the relevant unit and E2E tests.

Use ViewState transfer dispatch and wait for copied contents before assertions. Remove redundant commentary from the self-replace guard.
@l0gicgate

Copy link
Copy Markdown
Contributor

Reviewed and pushed through 2e5c76f (no force-push).

  • Merged verified lgse/strata:main at 4b894fead004de1e288a10fd1bc5409db9ca2b2d, preserving both independent test additions in the conflict.
  • Fixed six regression tests calling the old transfer API, and made copy assertions wait for completed contents rather than file creation/dialog dismissal.
  • Audited every added comment and removed the redundant self-replace narration. Corrected the description/test plan to match silent same-folder duplication and linked the prior issues.

Local pinned rootless-Podman verification: ./scripts/quality.sh fmt, ./scripts/quality.sh clippy, and ./scripts/quality.sh test passed (the required Cargo fmt/Clippy/all-targets/all-features commands; 1,472 tests passed, 20 existing ignores). STRATA_CONTAINER_ENGINE=podman ./scripts/e2e.sh --keep-artifacts passed all 811 cases. git diff --check passed.

GUI verification used real pointer/keyboard E2E on private Xvfb/D-Bus, including mixed-paste Skip, Keep Both, same-folder duplicates, replacement, cancellation, and undo; no separate manual desktop session was used. Review fixes do not change UI appearance. E2E captures are local under target/e2e-artifacts/; no new media was uploaded.

Approved the fork workflow run for this head; CI passed. No remaining review blockers found. The pull request has not been merged.

@l0gicgate

Copy link
Copy Markdown
Contributor

Guided desktop verification with the owner passed all six staged scenarios on 2e5c76f: mixed-paste Skip; Keep Both then Skip on the final conflict; silent same-folder paste and Duplicate; Replace; Cancel/Escape/dialog close; and partial move undo with Skip. The final undo filesystem state was also checked directly: the non-conflicting file returned, the skipped file stayed at its moved location, and the new occupant remained untouched. No code changes were needed.

@l0gicgate
l0gicgate merged commit b35298e into lgse:main Sep 11, 2026
21 checks passed
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.

3 participants