Skip to content

refactor!: audit the codebase, fix backup safety bugs, and speed up backups - #24

Merged
ishaanko merged 3 commits into
mainfrom
refactor/codebase-audit
Sep 23, 2026
Merged

ishaanko merged 3 commits into
mainfrom
refactor/codebase-audit

Conversation

@ishaanko

@ishaanko ishaanko commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Problem

An audit of every file found three kinds of problems:

  • Bugs that could lose or hide a backup copy, skip the game's final save, or report a false success.
  • Slow paths. One Git backup of a 3,000-file world started 3,029 git processes. A ZIP backup wrote its archive six times. Every start read every ZIP archive again.
  • Code that defended against problems a single-player backup tool does not have, duplicated helpers, dead code, and tests that pinned implementation details instead of behavior.

What this PR does

Safety fixes

Each fix has a test that failed before the fix. The end-to-end tests use real Git, real ZIP files, and real folders. The 0.4.0 section of CHANGELOG.md has the full list. The most important:

  • Cleanup no longer deletes the last copy of a backup while it says that another copy exists. It never offers the last intact copy of a labeled backup.
  • Cancel during a Git upload keeps the copies that are already complete.
  • An unreadable settings file is never replaced by defaults. Reset keeps a copy of the old file.
  • An error in WorldArchive while a world closes can no longer skip the game's final save.
  • One damaged object in a Git repository no longer makes every later Git backup fail. A damaged Git LFS file is set aside, not used again.
  • When a remote refuses a delete, nothing changes on the remote. A delete of an imported backup also removes it from the remote. A delete fails when a backup folder that you chose cannot be reached.
  • Menus no longer freeze while a backup runs.
  • git lfs install no longer runs on every backup. It could write hooks into the user's global Git hooks folder.
  • The schedule now skips a world that did not run since its last backup. Before, it never skipped.

Speed

Measured before and after on one machine:

Operation Before After
Git backup, 3,000-file world 3,029 git processes, 19 s 5 git processes, 0.10 s
ZIP backup, 305 MB world 3.3 GB read and written, 8.0 s 0.8 GB, 5.5 s, no temporary folder
World capture, closed world 624 ms 181 ms
Delete 50 backups 2.6 s, 402 git processes 119 ms, 10 git processes
Catalog rebuild at start, 300 backups 3.1 s about 30 ms

A backup of an open world still reads every file two times, because the game can write while the copy runs.

Less code

  • src/main and src/client went from 39,805 lines to 30,214 lines.
  • Shared helpers live in a new support package: one path validator, one inventory type, one manifest codec, one safe-text helper, and one file lock.
  • The runtime logic moved into src/main, where tests cover it. The Fabric side is a thin adapter.
  • The packages have no dependency cycles.

Tests

  • 486 tests became 372 tests. The suite runs in about 25 s.
  • The new e2e package runs the real engine through the production ServiceGraph.
  • Three golden ZIP archives written by 0.3.9 prove that the new reader still lists, verifies, and restores old archives.

Breaking change

WorldArchive no longer reads settings files from 0.1.0 or the shared Git repository of 0.1.0. CHANGELOG.md gives the steps to import that repository one time with Import > Repository.

Version

This PR is release 0.4.0. The release that had the number 0.4.0 (the move to Minecraft 26.3) is now 0.3.9 in CHANGELOG.md and in the code comments. mod_version stays 0.4.0. The old v0.4.0 tag and GitHub release are now v0.3.9, so v0.4.0 is free. After this PR merges, tag the merge commit v0.4.0.

Needs a check in the game

No test can run Minecraft. These 20 checks need ./gradlew runClient.

In-game checks

Start the game with ./gradlew runClient. Use a test world
with a few chunks explored. For the Git checks, install Git and Git LFS, and
make a bare repository with git init --bare <folder>/remote.git. Use its
absolute path as the world's Git remote on the Worlds settings tab.

Backups while you play

  1. Exit backup and its toast.

    1. Open a world, then choose Save and Quit to Title.
    2. Watch the toast.
    3. Open a large world, quit again, and click Cancel on the toast.

    Expected: the toast shows "Creating backup... Keep Minecraft open until it
    finishes.", then "Backup finished. You can safely quit Minecraft." in green.
    After Cancel, the toast shows "Cancelling backup...", then "Backup cancelled.
    Your world was saved."

  2. Manual backup of the open world keeps /save-off.

    1. In a world, run /save-off.
    2. Press Esc, click the WorldArchive icon, click Create, and confirm.
    3. Play for 6 minutes. Watch the game log.

    Expected: the backup succeeds. The log shows no autosave lines for the
    6 minutes, because autosave stays off as you set it.

  3. Schedule, pause, and a world that is off.

    1. On the Git and ZIP tabs, turn on the scheduled trigger and set
      the interval to 1 minute. Save.
    2. Walk around for 3 minutes.
    3. Press Esc and wait 3 minutes on the pause screen.
    4. On the Worlds tab, turn off Back up this world. Save, and play
      for 2 minutes.

    Expected: step 2 makes a backup each minute. Step 3 makes no backup and
    no chat line. Step 4 makes no backup and no chat line.

  4. Linked saves folder and linked world folder.

    1. Close the game. Move .minecraft/saves to another place and link it
      back: ln -s on Linux or macOS, mklink /J on Windows.
    2. Start the game. In Singleplayer, select a world and click
      Backups.
    3. Open the world. Press Esc and click the WorldArchive icon.
    4. Choose Save and Quit to Title.
    5. Do steps 1 to 4 again with one world folder moved and linked instead
      of the whole saves folder.

    Expected: the Backups button works, the icon opens the backup browser,
    and the exit backup is made, in both cases.

  5. Quit during an exit backup.

    1. Open a large world. Close the game window while the exit backup runs.
    2. Start the game again.

    Expected: "Saving world" stays for at most about 35 seconds. At the next
    start, the title screen shows one toast: "Minecraft closed before the
    world-exit backup finished, so that backup was not made." The toast does
    not come back after a second restart.

  6. A folder change while a backup runs, and the Create tooltip.

    1. Start a manual backup of a large world.
    2. While it runs, open Settings, change the ZIP folder, and click
      Save.
    3. After the backup, turn off Back up this world on the Worlds
      tab, save, and hover over Create in the backup browser.

    Expected: the save is refused with "Backup folders cannot change while a
    backup, restore, delete or import is running. Try again when it has
    finished." The Create tooltip says "Backups are off for this world.
    Turn them on in Settings, on the Worlds tab."

Backup browser

  1. Filter, rows, and reload.

    1. Open a world's backup browser. Type a word in the middle of the filter
      text and wait 2 seconds.
    2. Read one row. Hover over a row near the right edge and near the bottom
      edge of the screen.
    3. Open a world, open the browser from the pause screen, and leave it
      open while a scheduled backup runs.

    Expected: the cursor and the focus stay in the filter. A row reads
    "date · label · trigger". The tooltip stays inside the screen. The action
    buttons have a 4-pixel gap. The list reloads by itself when the scheduled
    backup finishes.

  2. Paging labels.

    1. Make the window small, about 854x480, with GUI scale 3.
    2. Open World Backups and an import preview with many backups.

    Expected: the page buttons read Previous and Next, not "<" and
    ">", and they do not overlap other buttons.

Delete

  1. Delete prompts and results.

    1. Sync one backup to the remote. Select it and click Delete.
    2. Select 3 backups, one with a label, and click Delete.
    3. Select 7 backups and click Delete. Use an 854x480 window at GUI
      scale 3.
    4. Confirm each prompt.

    Expected: the one-backup prompt says "Delete this backup from every
    destination? You cannot undo this." and "Its copy on your Git remote is
    deleted too." The 3-backup prompt names each backup by date and label and
    says "1 of them have a label. A label does not protect a backup from
    Delete." The 7-backup prompt ends with "...and 2 more". The buttons stay
    on the screen. The results say "Deleted 1 backup", "Deleted 3 backups",
    and "Deleted 7 backups" in green.

  2. A remote that refuses the delete.

    1. Run git --git-dir=<folder>/remote.git config receive.denyDeletes true.
    2. Delete one synced backup.

    Expected: the result says "No backups were deleted" in red. A line names
    the backup by date and label and gives the remote's reason. The backup
    stays in the list with its Git copy.

Sync, Verify, Restore, and Cancel

  1. Cancel buttons.

    1. Set the world's remote to an address that does not answer, for example
      ssh://git@10.255.255.1/remote.git. Select a backup and click Sync,
      then Cancel.
    2. Click Verify on a large backup, then Cancel.
    3. Click Create, then Cancel.
    4. Click Restore, confirm, then click Cancel while it writes.
    5. Delete a backup and look at the button while it runs.

    Expected: Sync shows "Cancelling sync...", then "Sync cancelled". Verify
    shows "Cancelling verification...", then "Verification cancelled". Create
    shows "Cancelling backup...", then "Backup cancelled". Restore shows
    "Cancelling restore...", then "Restore cancelled", and no new world folder
    is in saves. Delete shows "Please wait…" and has no Cancel.

  2. Restore prompts and names.

    1. In a world, press Esc, click the WorldArchive icon, select a backup,
      and click Restore.
    2. Type each of these names: con, a|b, x., and the folder name of
      the original world.

    Expected: the prompt has the line "This leaves the world you are
    playing." and a line about the Minecraft version of the backup. The name
    box refuses all four names.

  3. Restore from Edit World.

    1. In Singleplayer, select a world and click Edit, then
      Backups.
    2. Restore a backup and choose to show it in the world list.

    Expected: the new world and the original world are both in the list.

Storage and cleanup

  1. Storage limits and cleanup rows.

    1. Open Storage for a world. Change the Daily count, but do not
      save. Hover over Review Cleanup.
    2. Click Save. Type 1,5 as the limit and save again.
    3. Click Review Cleanup. Compare the preview rows with the
      confirmation rows.
    4. If the preview has protected backups with only a Git row, toggle one
      of them.

    Expected: Review Cleanup is off, and its tooltip says "Save the
    policy first; cleanup uses the saved limit". The limit 1,5 reads back as
    1.5. Preview and confirmation rows show the same "date · label · action ·
    N changed" text. Toggling one protected Git row toggles the whole group.

Import

  1. Import screens.

    1. Open World Backups, click Import, type https://user:pass@example.com/r.git
      into Repository address, and click Find Backups from Repository.
    2. Click Choose Backup Folder, then cancel the folder picker.
    3. Import a folder of ZIP archives in which some backups are already in
      the list.

    Expected: step 1 shows the reason, for example that the address must not
    contain a password. Step 2 shows "No folder was selected". In step 3 the
    status with conflicts is yellow, and it is green only for a clean import.

Settings

  1. Unreadable settings file.

    1. Close the game. Replace config/worldarchive.json with the text
      not json.
    2. Start the game. Open World Backups.
    3. Open Settings. Click Reset settings.
    4. Go back to World Backups without a restart.

    Expected: step 2 shows "WorldArchive settings could not be read (...), so
    backups are paused. Open Settings to fix or reset them." Step 3 shows
    "Settings could not be read: ..." and only Reset settings and
    Cancel. After the reset, the screen says "Settings were reset. The old
    file was kept as worldarchive.json.unreadable-..." and that file is in
    config. In step 4 the world list loads, and backups work again.

  2. Save failure and world problems.

    1. Make the config folder read-only. Change a setting and click
      Save.
    2. Make the folder writable again. On the Worlds tab, set a world's
      ZIP folder to a folder inside another world, and save.
    3. Close the game. Copy a world folder in saves. Start the game and open
      the Worlds tab.

    Expected: step 1 shows "Settings could not be saved: ..." with the
    reason. Step 2 marks the world red and shows its problem. Step 3 shows
    " is a copy of , so it now has its own backup history".

  3. Folder picker, footer, and text.

    1. Click Browse next to the Git folder. On Linux, also try it without
      an xdg-desktop-portal service.
    2. Read the footer on the Git and ZIP tabs.
    3. Read the help text under Git remote on the Worlds tab.
    4. Turn on Force Unicode Font in the language options and use a
      1280x720 window. Open the settings screen.

    Expected: the system folder dialog opens. Without a portal, the screen
    says "The native folder picker failed; type an absolute path instead".
    The footer shows Git, Git LFS, and folder state, and no "Remote" item.
    The help text says "Paste the address of this world's Git repository.
    Each new backup uploads to it. If an upload fails, choose Sync on that
    backup to try again." No text overlaps with Force Unicode Font.

World list integration

  1. Backups buttons and Open Folder.

    1. In Singleplayer, resize the window a few times.
    2. Select a world, click Edit, then Backups, then Done. Use
      Make Backup and Backups on the Edit World screen again.
    3. Turn off ZIP backups. Make a new world, open its backup browser, and
      click Open Folder. Then make a Git backup.

    Expected: there is always one Backups button, never two. The Edit
    World
    buttons still work after you come back. Open Folder opens the
    nearest existing folder and makes no empty <world id>.git folder. The
    Git backup succeeds.

  2. Screens changed in the final stage.

    1. On World Backups, click Settings, then Cancel.
    2. On World Backups, click Import, then Choose Backup Folder,
      and pick a folder of ZIP archives.
    3. Select a backup made with another Minecraft version and click
      Restore.
    4. Create, Sync, and Verify one backup each.

    Expected: step 1 opens the settings screen and returns to World
    Backups
    . Step 2 opens the folder dialog and then the import preview.
    Step 3 shows the version notice for the backup's version and the running
    version. Step 4 shows the same result headlines as before, for example
    "Backup completed", "Backup synchronized", and "Backup verified", with
    one line for each copy.

…ackups

An audit of every file found data-safety bugs, slow paths, and a lot of code
that defended against problems a single-player backup tool does not have.

- Fix cleanup, delete, cancel, settings, runtime, and Git bugs that could lose
  a copy, hide a copy, skip the game's final save, or report a false success.
  End-to-end tests with real Git and real ZIP files reproduce each one.
- Make a Git backup start a few git processes instead of one for each file,
  write each ZIP archive one time, copy the world with parallel workers, and
  batch deletes and the start-up catalog rebuild.
- Remove duplicate helpers, dead code, test-only hooks in production classes,
  and tests that pinned old implementation details. Move the runtime logic
  into src/main, where it has tests.

BREAKING CHANGE: WorldArchive no longer reads settings files from 0.1.0 or the
shared Git repository of 0.1.0. Import that repository one time with
Import > Repository.
@github-actions github-actions Bot added the size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs). label Sep 23, 2026
@greptile-apps

greptile-apps Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

Not safe to merge until the release uses a new, unused version tag.

Fix All in Claude CodeFindings

  1. P1 Reuse Blocks Release ▶
Fix with agent prompt
### Issue 1
CHANGELOG.md:3
This release is labeled `0.4.0`, but the existing `v0.4.0` tag points to the prior release rather than this commit. A normal push of the current release under that tag is rejected, so the tag-triggered release workflow cannot build and publish this artifact. Publish this change under a new unused version and align the changelog and build version with that tag.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
  • The release preparation reuses 0.4.0, even though v0.4.0 already identifies the prior release.
  • The current artifact cannot be published through the normal tag-based release process until it receives a new version.
  • The earlier cleanup recovery issues are fixed in the current code.

Reviews (3) · Last reviewed commit: "chore(release): make this release 0.4.0 ..."

Comment thread src/main/java/dev/ishaanko/worldarchive/storage/management/CleanupExecutor.java Outdated
@greptile-apps

greptile-apps Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P1 Existing v0.4.0 tag prevents releasing the current 0.4.0 artifact ▶

    • Bug
      • The current release commit declares version 0.4.0, but v0.4.0 already targets a prior commit. A normal push of current HEAD to that tag is rejected, so no qualifying tag-push event is delivered and the release workflow cannot publish the intended current artifact.
    • Cause
      • The release workflow is triggered solely by a pushed v*.*.* tag, and Git rejects updating an existing tag without force. The current release preparation commit reused version/tag v0.4.0 after that tag had already been created for the prior release.
    • Fix
      • Use a new, unused release version/tag for current HEAD (and align CHANGELOG.md and gradle.properties), or deliberately replace the remote tag only if rewriting the previously published release is explicitly intended and coordinated.

A failed ZIP delete counted as done when Files.exists returned false,
which it also does when the drive is away. A failed unmark also skipped
the catalog write that lists the kept copies again.

Keep every copy whose delete fails, and write the catalog even when the
deleted-backup list cannot be changed.
….3.9

The old 0.4.0 moved to Minecraft 26.3 and fixed bugs. This release
rewrites most of the engine and breaks old data, so it takes the 0.4.0
number.

Date the 0.4.0 changelog section, rename the old section to 0.3.9, and
make the comments that describe the old release say 0.3.9.
Comment thread CHANGELOG.md
@ishaanko
ishaanko merged commit d5291f0 into main Sep 23, 2026
3 checks passed
@ishaanko
ishaanko deleted the refactor/codebase-audit branch September 23, 2026 04:38
ishaanko added a commit that referenced this pull request Sep 29, 2026
…ackups (#24)

* refactor!: audit the codebase, fix backup safety bugs, and speed up backups

An audit of every file found data-safety bugs, slow paths, and a lot of code
that defended against problems a single-player backup tool does not have.

- Fix cleanup, delete, cancel, settings, runtime, and Git bugs that could lose
  a copy, hide a copy, skip the game's final save, or report a false success.
  End-to-end tests with real Git and real ZIP files reproduce each one.
- Make a Git backup start a few git processes instead of one for each file,
  write each ZIP archive one time, copy the world with parallel workers, and
  batch deletes and the start-up catalog rebuild.
- Remove duplicate helpers, dead code, test-only hooks in production classes,
  and tests that pinned old implementation details. Move the runtime logic
  into src/main, where it has tests.

BREAKING CHANGE: WorldArchive no longer reads settings files from 0.1.0 or the
shared Git repository of 0.1.0. Import that repository one time with
Import > Repository.

* fix(cleanup): keep a ZIP copy listed when its delete fails

A failed ZIP delete counted as done when Files.exists returned false,
which it also does when the drive is away. A failed unmark also skipped
the catalog write that lists the kept copies again.

Keep every copy whose delete fails, and write the catalog even when the
deleted-backup list cannot be changed.

* chore(release): make this release 0.4.0 and rename the old 0.4.0 to 0.3.9

The old 0.4.0 moved to Minecraft 26.3 and fixed bugs. This release
rewrites most of the engine and breaks old data, so it takes the 0.4.0
number.

Date the 0.4.0 changelog section, rename the old section to 0.3.9, and
make the comments that describe the old release say 0.3.9.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ effective changed lines (test files excluded in mixed PRs).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant