Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Never risk data loss without explicit user consent. A failed command that preser
- **Time-of-check vs time-of-use** — be conservative when there's a gap between the safety check and the operation. `wt merge` verifies clean before rebasing, but files could appear before cleanup — don't force-remove during cleanup.
- **Replace files, never truncate them** — `fs::write` truncates before it writes, so a crash mid-write leaves the file empty. Every write to a file worktrunk can't put back (rc files, shell wrappers, `config.toml`, `approvals.toml`, another tool's `settings.json`) goes through `utils::write_atomically`, which renames a sibling temp file over the target; the spec on that function covers symlinks, mode, and what a rename costs. Regenerable content (the cache, the `-vv` diagnostic report) keeps the plain write.

These stop where git's own protections stop: `wt merge` and `wt step push` overwrite an ignored file in the destination worktree whose path the incoming commits track, exactly as a `git merge` run there would. Matching git is deliberate — the spec in `src/commands/worktree/push.rs` says why.

Full inventory: FAQ [What files does Worktrunk create?](docs/content/faq.md#what-files-does-worktrunk-create) and [What can Worktrunk delete?](docs/content/faq.md#what-can-worktrunk-delete). Review new code that changes this surface against those sections.

## Command Execution Principles
Expand Down
5 changes: 5 additions & 0 deletions src/commands/repository_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ pub trait RepositoryCliExt {
/// The caller has already established that `target_worktree` exists on disk
/// (`MergeContext::prepare` refuses a registered-but-missing worktree), so
/// the status read here is free to fail if the directory is gone.
///
/// Ignored files are deliberately out of scope — they're absent from the
/// `git status --porcelain` read this works from, and matching git there is
/// the decision, not an oversight. The module spec in
/// `commands/worktree/push.rs` says why.
fn prepare_target_worktree(
&self,
target_worktree: Option<&PathBuf>,
Expand Down
12 changes: 12 additions & 0 deletions src/commands/worktree/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
//! Push changes to target branch with safety checks. Both fast-forward push and
//! `--no-ff` merge share common scaffolding (target resolution, fast-forward check,
//! stash guard, progress/success output) extracted into [`MergeContext`].
//!
//! # Destination-worktree safety follows git
//!
//! The checks cover what `git status --porcelain` reports: a modified or
//! untracked file at a path the push range changes is refused, and the rest of
//! a dirty tree is autostashed and restored. Ignored files fall outside that
//! report, so a push overwrites one whose path the incoming commits track.
//!
//! That is git's own line, not an oversight — a `git merge` run in that
//! worktree refuses the untracked collision and overwrites the ignored one.
//! Matching git is the decision; don't add an ignored-file probe to make `wt`
//! stricter than the tool it wraps.

use std::path::PathBuf;

Expand Down
Loading