Skip to content

Use detached commits when archiving worktrees#53458

Merged
rtfeldman merged 22 commits intomainfrom
persist-worktree-3-wiring
Apr 9, 2026
Merged

Use detached commits when archiving worktrees#53458
rtfeldman merged 22 commits intomainfrom
persist-worktree-3-wiring

Conversation

@rtfeldman
Copy link
Copy Markdown
Contributor

@rtfeldman rtfeldman commented Apr 8, 2026

Don't move the branch when making WIP commits during archiving; instead make detached commits via write-tree + commit-tree.

(No release notes because this isn't stable yet.)

Release Notes:

  • N/A

rtfeldman and others added 18 commits April 8, 2026 17:11
…ations

Extend the git API with several new capabilities needed for worktree
archival and restoration:

- Add allow_empty flag to CommitOptions for creating WIP marker commits
- Change create_worktree to accept Option<String> branch, enabling
  detached worktree creation when None is passed
- Add head_sha() to read the current HEAD commit hash
- Add update_ref() and delete_ref() for managing git references
- Add stage_all_including_untracked() to stage everything before a
  WIP commit
- Implement all new operations in FakeGitRepository with functional
  commit history tracking, reset support, and ref management
- Update existing call sites for the new CommitOptions field and
  create_worktree signature
This argument already gets added when calling git_binary.run()
Now creating a worktree from a detached head also has remote support
GitStore::stage_all already does everything this method does and updates
pending_ops as well. So I'm removing this method to avoid a potential
foot gun in our codebase
- resolve_commit on Repository entity: wraps revparse_batch to check
  if a commit SHA exists in the repo. Used during restore to verify
  original_commit_hash is present before attempting recovery.
- repair_worktrees on GitRepository trait + RealGitRepository (runs
  git worktree repair) and FakeGitRepository (no-op). Used during
  restore when a worktree directory exists on disk but may not be in
  git's worktree metadata.
- repair_worktrees wrapper on Repository entity.
- resolve_commit on Repository entity: wraps revparse_batch to check
  if a commit SHA exists in the repo. Used during restore to verify
  original_commit_hash is present before attempting recovery.
- repair_worktrees on GitRepository trait + RealGitRepository (runs
  git worktree repair) and FakeGitRepository (no-op). Used during
  restore when a worktree directory exists on disk but may not be in
  git's worktree metadata.
- repair_worktrees wrapper on Repository entity.
Add the persistence layer for tracking archived git worktrees:

- ArchivedGitWorktree struct with staged_commit_hash and
  unstaged_commit_hash fields to precisely identify WIP commits
- DB migrations for archived_git_worktrees and thread_archived_worktrees
  (join table) tables
- CRUD operations: create, link to thread, query by thread, delete
- Column impl for deserializing ArchivedGitWorktree from DB rows
- Tests for create/retrieve with distinct SHAs, delete cascading
  through join table, multi-thread linking, and multiple worktrees
  per thread
Connect the git API and archived worktree data model to the sidebar's
archive/unarchive flow:

- Add thread_worktree_archive module: orchestrates the full archive
  cycle (WIP commits, DB records, git refs, worktree deletion) and
  restore cycle (detached worktree creation, reset to recover
  staged/unstaged state, branch restoration)
- Integrate into sidebar: archive_thread now persists worktree state
  before cleanup; activate_archived_thread restores worktrees via git
  with targeted path replacement for multi-root threads
- Show toast on restore failure instead of silent log
- Deserialize persisted project_group_keys on window restore
- Guard cleanup_empty_workspaces against dropped entities
- Await rollback DB operations instead of fire-and-forget
- If worktree already exists on disk when unarchiving, reuse it as-is
  instead of auto-generating a new path
persist_worktree_state:
- Read HEAD SHA before creating WIP commits as original_commit_hash
- Pass it to create_archived_worktree

restore_worktree_via_git:
- Pre-restore: verify original_commit_hash exists via resolve_commit;
  abort with user-facing error if the git history is gone
- Worktree-already-exists: check for .git file to detect if path is a
  real git worktree; if not, call repair_worktrees to adopt it
- Resilient WIP resets: track success of mixed and soft resets
  independently; if either fails, fall back to mixed reset directly to
  original_commit_hash
- Post-reset HEAD verification: confirm HEAD landed at
  original_commit_hash after all resets
- Branch restoration: after switching, verify branch points at
  original_commit_hash; if it doesn't, reset and create a fresh branch
Complete the thread archival refactor by implementing Phases 3-4:

Phase 3: Sidebar::archive_worktree (the executor)
- New async method that safely tears down workspaces, git state, and FS paths
- Prompts user to save/discard dirty items, racing against cancellation
- Closes workspace via MultiWorkspace::remove while retaining Project ref
- Iterates over roots: persists git state then removes worktrees
- Full rollback support: if any step fails or is cancelled, all completed
  persists are rolled back in reverse order

Phase 4: Sidebar::archive_thread (the orchestrator)
- Reads thread metadata to determine which roots need cleanup
- Filters out roots still referenced by other unarchived threads
- Creates cancel channel and spawns archive_worktree as background task
- Passes (Task, Sender) to ThreadMetadataStore::archive for tracking
- On success: cleans up completed archive entry
- On user cancel or error: automatically unarchives thread
- Preserves all existing focus management code

Additional fixes:
- Simplify ThreadMetadataStore::archive to accept pre-built (Task, Sender)
  instead of generic closure, removing unnecessary type parameters
- Fix pre-existing workspaces() API change (iterator vs slice)
- Fix pre-existing resolve_commit removal (deleted dead code path)
- Remove unused window_for_workspace helpers
- Add ArchiveStatus enum for distinguishing success vs user cancellation
Use futures::channel::oneshot for the archive cancellation signal and
futures::channel::mpsc::unbounded for the DB operations queue, replacing
smol::channel usage. Remove the smol dependency from the sidebar crate.
Rewire sidebar thread archival to start the in-flight worktree archive
task again, so archiving the last thread for a linked worktree removes
the worktree from disk instead of only updating sidebar state.

This also makes root planning and worktree-state persistence more
robust when a live linked-worktree repository handle is not already
available, and strengthens the sidebar test to assert the worktree
directory is actually deleted.
Restore smol::channel for the DB operations queue and archive
cancellation signal, reverting the futures::channel change from
e1c73e7. smol channels provide bounded backpressure and are a
better fit here.
Replace the old approach (git commit + git reset) with detached commits
via write-tree + commit-tree. This means the branch is never moved
during archival, eliminating the bug where WIP commits remained on the
branch after unarchive because git switch moved HEAD back to the
branch tip.

Archive: create_archive_checkpoint writes the current index tree and a
temp-index full tree as two detached commit objects, without touching
HEAD or the branch ref.

Restore: create worktree at original_commit_hash (branch unchanged),
switch to branch (no-op since it still points there), then read-tree +
git restore to reconstruct staged/unstaged state from the WIP trees.

Rollback: simplified to just deleting the git ref and DB record, since
no branch was moved.
@rtfeldman rtfeldman self-assigned this Apr 8, 2026
@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Apr 8, 2026
@zed-community-bot zed-community-bot bot added the staff Pull requests authored by a current member of Zed staff label Apr 8, 2026
@rtfeldman rtfeldman changed the title Wire up worktree archival on thread archive and restoration on unarchive Use detached commits for worktree archival and wire up on-disk cleanup Apr 8, 2026
@rtfeldman rtfeldman force-pushed the persist-worktree-3-wiring branch from dc5226c to 0cacec5 Compare April 8, 2026 22:27
@rtfeldman rtfeldman changed the title Use detached commits for worktree archival and wire up on-disk cleanup Use detached commits for worktree archival Apr 8, 2026
@rtfeldman rtfeldman changed the title Use detached commits for worktree archival Use detached commits when archiving worktrees Apr 8, 2026
@rtfeldman rtfeldman marked this pull request as ready for review April 8, 2026 22:56
@rtfeldman rtfeldman force-pushed the persist-worktree-3-wiring branch from 7fbad98 to 8097f24 Compare April 9, 2026 00:03
- Fix restore_archive_checkpoint to handle file deletions by using
  read-tree --reset -u for the unstaged tree before a bare read-tree
  for the staged index
- Make git ref creation fatal in persist_worktree_state so archives
  don't silently lose data when gc runs
- Add cleanup logic in restore_worktree_via_git to remove newly-created
  worktrees if a later step fails
- Always run restore_archive_checkpoint even when the worktree directory
  already exists, so interrupted restores complete on retry
- Remove PersistOutcome struct, just pass the i64 DB row ID directly
- Log the original change_branch error before falling back to
  create_branch
- Fix .unwrap() in FakeGitRepository::restore_archive_checkpoint
- Remove dead stage_all_including_untracked from trait and impls
- Remove dead commit_exists from git_store::Repository
- Replace fragile 3x run_until_parked + polling loop in sidebar test
  with a single consolidated retry loop
- Replace all matching paths in complete_worktree_restore, not just
  the first
@rtfeldman rtfeldman force-pushed the persist-worktree-3-wiring branch from 8097f24 to 0581577 Compare April 9, 2026 00:04
@rtfeldman rtfeldman enabled auto-merge (squash) April 9, 2026 00:05
@rtfeldman rtfeldman merged commit 7544515 into main Apr 9, 2026
31 checks passed
@rtfeldman rtfeldman deleted the persist-worktree-3-wiring branch April 9, 2026 00:25
@rtfeldman
Copy link
Copy Markdown
Contributor Author

/cherry-pick preview

@zed-zippy
Copy link
Copy Markdown
Contributor

zed-zippy bot commented Apr 9, 2026

🍒💥 Cherry-pick did not succeed
https://github.com/zed-industries/zed/actions/runs/24213906742

rtfeldman added a commit that referenced this pull request Apr 10, 2026
Don't move the branch when making WIP commits during archiving; instead
make detached commits via `write-tree` + `commit-tree`.

(No release notes because this isn't stable yet.)

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
maxbrunsfeld added a commit that referenced this pull request Apr 10, 2026
Cherry-picked PRs (in order applied):

1. #53386
2. #53400
3. #53396
4. #53428
5. #53356
6. #53215
7. #53429
8. #53458
9. #53436
10. #53451
11. #53454
12. #53419
13. #53287
14. #53521
15. #53463
16. #52848
17. #53544
18. #53556
19. #53566
20. #53579
21. #53575
22. #53550
23. #53585
24. #53510
25. #53599
26. #53099
27. #53662
28. #53660
29. #53657
30. #53654


Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Cameron Mcloughlin <cameron.studdstreet@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: ojpro <contact@ojpro.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement staff Pull requests authored by a current member of Zed staff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants