Skip to content

Add guarded conflict recovery plans#72

Open
cosentinode wants to merge 59 commits into
developfrom
issue-55-recovery-policy
Open

Add guarded conflict recovery plans#72
cosentinode wants to merge 59 commits into
developfrom
issue-55-recovery-policy

Conversation

@cosentinode

Copy link
Copy Markdown
Owner

Summary

  • add typed merge/rebase recovery requests, parser commands, high-risk plans, and fixed audit names
  • centralize conflict-mode allowlisting and preflight every prompt-sequence step before execution
  • snapshot exact recovery state at preview and revalidate it immediately before typed Git recovery
  • preserve status, diff, fetch, branch listing, and staging workflows during conflict resolution

Verification

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace

Fixes #55

@cosentinode

Copy link
Copy Markdown
Owner Author

Fresh-context review found two blocking concerns:

  1. High: queued operations can bypass conflict mode after preview (crates/bitbygit-tui/src/lib.rs:1649, crates/bitbygit-tui/src/lib.rs:2833). ensure_conflict_mode_allowed runs while the plan is prepared, but PlanExecutor::execute runs that queued plan after confirmation without checking the policy again. If a merge/rebase starts between preview and confirmation, an already queued unrelated operation can still execute; for example, push remains valid to Git during a conflicted merge and will produce its remote side effect. The first prepared step of a prompt sequence has the same gap, even though deferred steps are replanned. Revalidate conflict mode immediately before executing every prepared operation (including the first sequence step), and add a test that queues while clean, introduces a conflict, then confirms and verifies no side effect.

  2. High: the “exact” recovery snapshot omits the state that controls recovery (crates/bitbygit-git/src/lib.rs:707-718). RecoveryState records the operation label, HEAD, porcelain status, index entries, and worktree diff, but not Git control metadata such as MERGE_HEAD, MERGE_MSG, MERGE_MODE, ORIG_HEAD, or the active rebase-merge/rebase-apply contents. Those files can change after preview without changing any compared field, while changing the merge parents/message, abort destination, or rebase todo/current commit. recover_exact will therefore accept materially different recovery state and run the high-risk action. Snapshot/revalidate the relevant operation metadata and add mutation tests for merge and rebase control state.

Verification: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, and cargo test --locked --workspace all pass locally; CI is also green.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed both blocking review concerns in commit 0fdb1e4:

  • Revalidate conflict-mode policy at PlanExecutor::execute immediately before any prepared operation runs. This covers standalone queued confirmations, the first already-prepared prompt-sequence step, and later replanned steps. The regression queues a sequence while clean, starts a conflicting merge, confirms, and verifies the first-step push does not change the remote or start an audit.
  • Extend RecoveryState with deterministic snapshots of merge/rebase control metadata, including merge control files, ORIG_HEAD, REBASE_HEAD, and recursive contents of both rebase backend directories. Merge-message and rebase-todo mutation regressions verify recovery remains blocked.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace

@cosentinode

Copy link
Copy Markdown
Owner Author

High: exact rebase recovery still ignores refs that abort can overwrite (crates/bitbygit-git/src/lib.rs:23-33, crates/bitbygit-git/src/lib.rs:718-730). RecoveryState snapshots HEAD/status/index/worktree and selected control files, but not the original branch ref named by rebase-merge/head-name (or rebase temporary refs such as refs/rewritten). During a conflicted rebase I moved refs/heads/topic with git update-ref; HEAD plus the captured status/index/worktree inputs remained byte-for-byte unchanged, and none of the listed metadata paths changes. recover_exact(Rebase, Abort, ...) therefore accepts the stale preview, after which git rebase --abort resets topic to the saved original OID and overwrites the newer ref. This violates the requirement that repository changes after preview block execution and can discard concurrent branch state. Snapshot and revalidate the operation-owned/referenced refs (at least the branch ref named by the active rebase and refs/rewritten for rebase-merges), and add an abort regression that mutates the original branch ref after preview and verifies both that recovery is blocked and that the newer ref remains intact.

Verification: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, and cargo test --locked --workspace all pass at 0fdb1e4; CI is green.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed the exact rebase recovery ref concern from #72 (comment) in commit c8f0f9a.

  • RecoveryState now snapshots the original branch ref named by either active rebase backend’s head-name metadata.
  • It also snapshots the full refs/rewritten namespace during rebases, including ref names, object IDs, and symbolic targets.
  • Added an abort regression that moves refs/heads/topic after preview and verifies exact recovery is blocked while the newer branch ref remains intact.
  • Added a --rebase-merges regression proving changes under refs/rewritten also block exact recovery.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace

@cosentinode

Copy link
Copy Markdown
Owner Author

High: deferred sequence requests are revalidated only after the first side effect (crates/bitbygit-tui/src/lib.rs:2849-2860, crates/bitbygit-tui/src/lib.rs:3498-3518). At confirmation, PlanExecutor::execute validates only the prepared first plans request, then PromptSequenceExecutor executes it before replanning any remaining request. For example, queue fetch && push while clean, start a conflicting merge before confirmation, and confirm: fetch is conflict-allowed, so it runs and can update remote-tracking refs and write audit entries; only afterward is the deferred push planned and rejected for the active merge. This violates the issue requirement that a prompt sequence containing a blocked operation fail before any side effect. The new queued-sequence regression uses push as step 1, so it does not cover an allowed, side-effecting first step followed by a newly blocked deferred step. Revalidate conflict policy for the complete queued request list immediately before executing step 1, and add a regression for this ordering that verifies neither refs nor audit state change.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed the deferred sequence policy bypass from #72 (comment) in commit 168f0dc.

  • PromptSequenceExecutor now revalidates the complete queued request list, including deferred requests, immediately before executing the prepared first step.
  • Added a fetch && push regression that queues while clean, advances the remote, starts a conflicting merge, then confirms and verifies the remote-tracking ref and audit state remain unchanged.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace

@cosentinode

Copy link
Copy Markdown
Owner Author

High: exact rebase recovery can overwrite ignored worktree data changed after preview (crates/bitbygit-git/src/lib.rs:718-732). RecoveryState captures default porcelain status, index entries, and a tracked worktree diff, but default status omits ignored paths and git diff does not hash their contents. I reproduced this at 168f0dc with a two-commit rebase stopped on commit 1: commit 2 in the original branch tracks target/victim.bin, while /target/ is ignored in the stopped state. After preview, creating or changing the ignored target/victim.bin leaves HEAD, status, ls-files --stage, worktree diff, recovery metadata, and recovery refs unchanged, so recover_exact accepts the stale snapshot; git rebase --abort then silently replaces that file with the original branch blob. This violates the acceptance criterion that repository changes after preview block execution and causes loss of post-preview data. Snapshot/revalidate ignored worktree paths/content that the recovery destination can overwrite, or otherwise fail closed on destination collisions, and add a rebase-abort regression proving the changed file is preserved.

Verification: cargo test --locked --workspace passes locally; git diff --check passes; Rust CI and GitGuardian are green.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed concern #4973048113 in commit 025b34f (fix: protect ignored recovery data).

RecoveryState now snapshots ignored untracked worktree paths, including file contents and filesystem type, so recover_exact detects ignored data created, removed, or changed after preview and fails closed before Git recovery can overwrite it. Added exact_rebase_abort_preserves_ignored_file_changed_after_preview, reproducing a two-commit rebase where the original branch tracks target/victim.bin while it is ignored at the stopped step; changing the file after preview blocks abort and preserves its changed contents.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace
  • Focused regression: cargo test --locked -p bitbygit-git exact_rebase_abort_preserves_ignored_file_changed_after_preview

@cosentinode

Copy link
Copy Markdown
Owner Author

Fresh-context review of current head 025b34f found three blocking concerns:

  1. High: exact rebase recovery omits refs scheduled by --update-refs (crates/bitbygit-git/src/lib.rs:772-795). The recursive metadata snapshot captures rebase-merge/update-refs as bytes, but recovery_refs only snapshots the original head-name ref and refs/rewritten; it does not snapshot the current values of the additional branch refs named in update-refs. I reproduced this at the current head with a side branch scheduled by git rebase --update-refs --exec false: after the preview-equivalent snapshot, moving the side branch left HEAD, porcelain status, index, worktree diff, rebase metadata, the topic ref, and refs/rewritten unchanged. Recovery therefore accepts the stale state; git rebase --continue advances topic and clears the rebase, merely printing that the concurrently moved side ref could not be updated. This violates the requirement that post-preview repository changes block execution and permits partial recovery side effects. Snapshot/revalidate every ref named by the active rebase metadata (including update-refs) and add a regression that moves such a ref after preview and verifies continue is blocked before HEAD/operation state changes.

  2. High: recovery snapshots read every ignored file fully into RAM (crates/bitbygit-git/src/lib.rs:736-756, crates/bitbygit-git/src/lib.rs:1276-1281). ignored_worktree enumerates the repository-wide ignored set and stores each file as File(Vec<u8>) in the queued plan. At confirmation, recover_exact builds a second full snapshot while the first is still resident. A normal ignored build tree can therefore make recovery scan and retain gigabytes twice; a very large sparse ignored file can trigger a huge logical-size allocation and terminate the TUI despite consuming little disk. Scope protection to paths the selected recovery can overwrite and/or use bounded streaming fingerprints rather than retaining all ignored contents, with a large/sparse-file regression.

  3. Medium: exact recovery still does not detect content changes to non-ignored untracked files (crates/bitbygit-git/src/lib.rs:718-732). Porcelain status records an existing untracked path but not its bytes, ls-files --stage and git diff exclude it, and the new content snapshot explicitly selects only --ignored paths. Editing an already-present non-ignored untracked file after preview therefore leaves RecoveryState equal and recovery proceeds, contrary to the acceptance criterion that repository changes after preview block execution. Cover non-ignored untracked content with the same bounded strategy and add a mutation regression.

Verification at 025b34f: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo test --locked --workspace, and git diff --check pass. CI and GitGuardian are green.

@cosentinode

Copy link
Copy Markdown
Owner Author

Implemented all three blocking fixes in 691dd2f (fix: bound exact recovery snapshots).

  • Active rebase recovery now parses both rebase backends' update-refs metadata and snapshots every named ref, deduplicated with the original topic ref and refs/rewritten. The regression moves a scheduled side branch after preview and verifies rebase --continue is blocked before HEAD or operation state changes.
  • Ignored regular files are now represented by bounded streaming Git object fingerprints plus file size instead of retained file contents. The regression snapshots and revalidates a 256 MiB sparse ignored file while asserting only the bounded fingerprint is retained.
  • Non-ignored untracked regular files now use the same fingerprint snapshot, so content-only edits to an already-present untracked path block exact recovery.
  • Symlink targets and special-file identity remain fail-closed in the untracked/ignored snapshot, while path enumeration is deterministic and covers both ignored and non-ignored untracked files.

Exact verification completed successfully:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (all workspace tests pass; bitbygit-git: 72 passed)
  • git diff --check

Post-push status is clean and HEAD matches origin/issue-55-recovery-policy at 691dd2feae240cc34cb5f781fa0d63f4452c1839.

@cosentinode

Copy link
Copy Markdown
Owner Author

High: confirmation-time recovery revalidation is a non-coherent snapshot and can miss concurrent edits (crates/bitbygit-git/src/lib.rs:272-287, crates/bitbygit-git/src/lib.rs:718-732). recovery_state() reads HEAD/status/index/diff first, then may spend substantial time walking and hashing the untracked/ignored tree before reading metadata and refs. If an editor or another Git process changes a tracked file after lines 727-729 have captured the old diff but before the slow scan completes, the assembled state still equals the preview even though the live worktree no longer does; recover_exact then performs a separate recover call and an abort can overwrite that post-preview edit. Ref/worktree changes in the additional compare-to-act window have the same problem. Revalidation needs a coherent/fenced strategy, or at minimum a final recheck of every overwrite-sensitive input after the slow work and atomic expected-value updates for refs, with a synchronized concurrency regression.

High: snapshots remain unbounded in memory and work despite the new sparse-file test (crates/bitbygit-git/src/lib.rs:727-730, crates/bitbygit-git/src/lib.rs:736-803, crates/bitbygit-git/src/lib.rs:1593-1602). Tracked changes are still materialized as the complete git diff --binary output in Vec<u8>; at confirmation both the queued and newly built copies coexist, so a large incompressible tracked binary can consume multiple times its size and terminate the TUI. For untracked/ignored data, hash-object bounds retained bytes but still reads every byte and launches one Git process per file, repository-wide, at preview and again at confirmation. A normal large target/ or node_modules/ tree can therefore require tens of thousands of subprocesses and many GB of I/O. The 256 MiB sparse regression only checks retained OID length and itself hashes all logical bytes twice; it does not establish a resource bound. Scope the scan to paths recovery can overwrite and use a batched, explicitly bounded/fail-closed representation; add large tracked-binary and high-file-count coverage.

Medium: untracked/ignored filesystem changes are not captured completely (crates/bitbygit-git/src/lib.rs:782-815, crates/bitbygit-git/src/lib.rs:1605-1617). A regular file is represented only by size and content OID, with no mode/permissions, while every non-file/non-symlink type collapses to Other. Changing an ignored recovery-destination file from non-executable to executable after preview leaves the full RecoveryState equal, and rebase abort can then replace it with the original tracked mode, silently losing the post-preview change. Replacing one special type with another is likewise invisible. Include recovery-relevant filesystem metadata/type identity (or fail closed on such entries) and add an ignored-file chmod regression proving abort is blocked and the new mode survives.

Verification at current head 691dd2f: cargo test --locked --workspace passes locally (207 tests), git diff --check passes, and both GitHub checks are green.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed the recovery snapshot review in commit 433d8c0.

Design changes:

  • Replaced repository-wide status/binary-diff/untracked snapshots with SHA-256 fingerprints of only paths changed in the active operation or its saved recovery destination. Rebase path derivation uses the active backend's snapshotted orig-head; unrelated ignored trees are not traversed.
  • Added fail-closed limits for retained state (8 MiB), Git output (4 MiB), file content read (64 MiB), relevant files (10,000), relevant path bytes (256 KiB), recovery metadata (4 MiB / 4,096 entries), and Git subprocesses (12). Output is bounded while read, metadata traversal is iterative and globally bounded, and hashing is in-process rather than one subprocess per file.
  • Confirmation now performs two complete matching captures before invoking Git, so an edit synchronized into the former slow-scan window is caught by the final capture. Recovery metadata and operation-owned/referenced refs remain guarded.
  • Regular-file fingerprints include size, SHA-256, mode, and filesystem identity. Unsupported types fail closed; Unix opens use O_NOFOLLOW | O_NONBLOCK plus handle/path identity revalidation to avoid symlink/FIFO races.

Regression coverage includes the synchronized concurrent edit, an actual 64 MiB tracked binary bound, a 10,001-file ignored tree, relevant file-count and metadata-entry limits, ignored content and chmod changes, file-to-directory replacement, incorrect top-level ORIG_HEAD, and FIFO/symlink replacement races.

Verification passed at 433d8c0:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (214 tests)
  • git diff --check

Git does not expose a transaction spanning arbitrary external worktree edits and git merge/rebase --continue|--abort|--skip; this removes the reported non-coherent slow-scan window with a final bounded recheck, while an external process racing after that final check remains a narrow platform-independent limitation.

@cosentinode

Copy link
Copy Markdown
Owner Author

High: a relevant directory is snapshotted without its contents, so abort can delete post-preview data (crates/bitbygit-git/src/lib.rs:843-920). When a recovery-relevant path is a directory, snapshot_recovery_worktree records only RecoveryWorktreeValue::Directory; it neither fingerprints descendants nor fails closed. I reproduced the resulting Git behavior with a rebase stopped before a commit that replaces an ignored victim/ directory with a tracked regular file victim: create victim/data before the preview, change it after the preview, then abort. Both guard captures still see only victim: Directory, so recover_exact accepts the stale preview; git rebase --abort removes the directory and restores the tracked file, deleting the changed victim/data. This violates the post-preview revalidation requirement and causes data loss. Snapshot directory descendants with the same explicit bounds (including metadata/content), or fail closed whenever recovery can replace a nonempty directory, and add a regression for a destination file colliding with an ignored directory whose child changes after preview.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed #72 (comment) in commit 0595603 (fix: protect recovery directory contents).

  • Recovery-relevant directories now expand into recursively fingerprinted descendant entries instead of recording only the top-level directory node. Descendant regular-file contents and metadata use the existing bounded fingerprint path, and directory mode/identity are retained.
  • Recursive expansion remains fail-closed under the existing relevant-entry, path-byte, retained-memory, and file-read limits.
  • Added exact_rebase_abort_preserves_ignored_directory_child_changed_after_preview, covering a recovery destination regular file colliding with an ignored directory. Changing the directory child after preview now blocks abort, leaves the rebase active, and preserves the changed child.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (all workspace tests pass; bitbygit-git: 80 passed)
  • cargo test --locked -p bitbygit-git exact_rebase_abort_preserves_ignored_directory_child_changed_after_preview
  • git diff --check

@cosentinode

Copy link
Copy Markdown
Owner Author

High: final recovery validation still has an unguarded compare-to-act window that can lose concurrent state (crates/bitbygit-git/src/lib.rs:283-310). recover_exact takes a second matching snapshot, but then calls recover, which runs another full status()/operation check before it finally spawns git rebase --abort (or the requested recovery command). Nothing locks the guarded worktree/index/refs or supplies expected values to that command. If another process moves the original rebase branch after the second snapshot returns, status() still accepts the active rebase and git rebase --abort can reset that branch to orig-head, discarding the concurrent ref update; an edit to an overwrite-sensitive worktree path in the same interval can likewise be replaced. The synchronized regression only edits during the first confirmation-time capture and proves that the second capture catches that earlier window; it does not cover an edit after the final capture. Close or explicitly fence the final snapshot-to-command interval (including operation-owned refs), and add a synchronized regression that mutates the original branch after final validation but before Git recovery and verifies the newer ref is preserved and recovery does not proceed.

Verification at 0595603: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo test --locked --workspace, and git diff --check pass; both GitHub checks are green.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed #72 (comment) in commit 00e13d5 (fix: fail closed at recovery execution).

Design:

  • Re-evaluated the compare-to-act design and removed recovery execution from the production path. Git does not provide a transaction spanning operation-owned refs, recovery metadata, the index, and arbitrary worktree paths, and its lock files cannot be pre-held and transferred to git merge/rebase --continue|--abort|--skip. An application/advisory lock would not fence the concurrent Git/editor process in the finding.
  • recover_exact retains its preview/final validation diagnostics but now fails closed at the act boundary without spawning Git. The unguarded low-level recovery executor is test-only, so production callers cannot bypass the guarded API.
  • TUI execution records the blocked recovery as an error and leaves the active operation untouched.

Deterministic regression:

  • exact_rebase_abort_fails_closed_on_branch_move_after_final_validation synchronizes a second process at a hook placed after the final guard capture and before the act boundary, moves refs/heads/topic, and verifies the newer OID remains, the rebase remains active, and recovery does not run.
  • Existing success-path coverage was updated to assert the deliberate fail-closed behavior and stable error auditing.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (216 tests)
  • git diff --check
  • Focused Git regression and TUI fail-closed audit regression both pass.

Risk/tradeoff: confirmed merge/rebase recovery actions are intentionally unavailable through BitByGit rather than exposing a data-loss race. Recovery must be performed outside BitByGit until an actual cross-resource fence is implemented.

@cosentinode

Copy link
Copy Markdown
Owner Author

High: every recovery action is permanently non-functional (crates/bitbygit-git/src/lib.rs:284-318, crates/bitbygit-tui/src/lib.rs:3223-3247). After both state captures match, recover_exact unconditionally returns GitError::Blocked; the only method that actually runs git merge/rebase --continue|--abort|--skip is now #[cfg(test)]. Consequently uppercase-Y confirmation of every newly exposed recovery plan always records an error and leaves the operation active. This directly violates the issue goal of guarded recovery and the repository requirements to “allow explicit continue, abort, and skip actions” and make conflict states “recoverable from the TUI” (docs/guardrails.md:110-123, docs/mvp-phases.md:222-233). The regression suite currently codifies the broken behavior in confirmed_recovery_fails_closed_and_records_stable_audit_entries (crates/bitbygit-tui/src/lib.rs:5064-5091) instead of covering successful production execution. Keep the preview/execution-time revalidation, but invoke the typed recovery command when the state is unchanged and restore end-to-end success coverage for merge/rebase continue, abort, and rebase skip.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed both #72 (comment) and #72 (comment) in commit 9ae25d0 (fix: execute recovery with ref fencing).

Design:

  • recover_exact now executes the typed git merge/rebase --continue|--abort|--skip command after bounded preview/execution revalidation instead of unconditionally failing closed.
  • A final post-synchronization capture rejects the demonstrated branch move before Git starts, preserving HEAD, the conflicted worktree, operation metadata, and the newer branch OID.
  • Recovery runs with a temporary reference-transaction hook that rechecks every snapshotted recovery-owned/referenced ref in Git’s prepared phase, while the refs are locked. A later branch update therefore aborts Git’s ref transaction rather than being overwritten. Existing configured hooks are forwarded, and displayed Git arguments omit the temporary hook path.
  • Temporary fencing hooks are removed after execution.

Deterministic coverage:

  • Git-boundary success coverage executes merge continue/abort and rebase continue/abort/skip through recover_exact.
  • TUI coverage confirms all five requests execute successfully and merge abort records stable successful audit entries.
  • The synchronized post-validation rebase-abort race moves refs/heads/topic and verifies recovery is blocked with HEAD, worktree, active rebase, and the newer branch ref preserved.
  • Production-path merge continue coverage verifies configured commit hooks and signing behavior are not bypassed.

Verification passed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (218 tests)
  • git diff --check

Residual risk: Git still cannot transactionally lock arbitrary editor writes to worktree files. The bounded repeated state captures continue to detect those changes before spawn; the new prepared transaction fence specifically closes the demonstrated recovery-ref overwrite path without disabling recovery.

@cosentinode

Copy link
Copy Markdown
Owner Author

High: the ref fence can reject only after recovery has already mutated the repository (crates/bitbygit-git/src/lib.rs:323-330, crates/bitbygit-git/src/lib.rs:1575-1585). The generated hook checks protected refs only in the prepared phase, but git rebase --abort resets the index/worktree before it attempts the original-branch ref transaction. I reproduced this at the current head by stopping a rebase on a conflict, moving refs/heads/topic, and rejecting the abort ref transaction: git rebase --abort failed, yet the conflict index/worktree had already been replaced with the original topic contents while detached HEAD and rebase-merge remained active. Thus a branch move in the post-capture window is not fail-closed; the hook leaves a partially aborted operation and can discard conflict-resolution state. exact_rebase_abort_preserves_branch_move_after_final_validation changes the ref before the third capture, so it exits before Git starts and never exercises the new transaction fence. Add a race synchronized inside the actual prepared hook and require HEAD, index, worktree, metadata, and refs all to remain unchanged; the current hook-after-reset design cannot provide that guarantee.

High: overwrite-sensitive worktree/index state is still unfenced after the final capture (crates/bitbygit-git/src/lib.rs:312-330). After execution_state is captured, an editor or Git process can change a relevant file or the index before the recovery subprocess resets/applies it. The reference-transaction hook observes only ref updates and cannot detect or protect those changes, so abort/continue/skip can silently overwrite post-validation data. The synchronized worktree test mutates during an earlier capture, not after the final capture or during Git execution. This still violates the issue requirement that post-preview state changes block execution and needs a true execution-boundary strategy or explicit fail-closed behavior where no such fence exists.

High: installing the temporary hook can overwrite files in the configured hooks directory (crates/bitbygit-git/src/lib.rs:1534-1563). On Unix every sibling from the configured hooks directory is symlinked into the temporary directory. If that directory already contains expected-refs, line 1560 follows the symlink and truncates the original file with BitByGit state; if it contains transaction-input, the generated hook later truncates that original file via cat > "$input". Hook support files may use arbitrary names, so recovery must reserve private non-colliding storage outside the forwarded hook namespace and must never write through forwarded symlinks. Add regressions proving configured hook-directory contents remain byte-for-byte unchanged.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed every finding in commit cbd47eb:

  1. Ignored artifacts: ignored worktree contents are no longer read or subject to the 64 MiB data cap. Recovery now streams and hashes the complete NUL-delimited ignored path set with bounded retained memory and no output-size cap, preserving collision/path-change detection. recovery_fingerprint_streams_ignored_paths_without_reading_data verifies a 128 MiB ignored file plus 4,000 generated artifacts (well over the former 64 KiB retained path limit) can be fingerprinted, while exact_rebase_abort_rejects_changed_ignored_collision_path verifies path changes still block exact recovery.
  2. Mutation timeout/output termination: bounded commands now distinguish diagnostics, digest-only probes, and recovery execution. The deadline is enforced through the final pre-spawn check, but after mutating Git is spawned neither deadline expiry nor excess output kills it; output continues to drain while only the configured prefix is retained. recovery_execution_is_not_killed_after_spawn_limits exercises both an expired post-spawn deadline and oversized output and verifies successful completion with truncation. Diagnostic timeout/descendant cleanup tests remain passing.
  3. Process-safety fence: all recovery state previews now use RecoveryInputFence; RecoveryInputFence::validate performs the executable-process safety scan as part of final fenced validation, and non-exact recovery repeats the scan in its final pre-spawn callback. recovery_preview_rechecks_process_safety_inside_state_fence installs an executable hook during fingerprinting and verifies preview rejects it rather than accepting that fingerprint.
  4. Git 2.42 requirement: guarded recovery now performs an explicit capability/version check and returns a clear recovery requires Git 2.42 or newer error before using GIT_ATTR_SYSTEM/GIT_ATTR_GLOBAL. The requirement is documented in README.md and docs/guardrails.md; recovery_reports_clear_minimum_git_version covers Git 2.41 rejection, Git 2.42 acceptance, and a Windows-style version suffix.

Verification completed:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked -p bitbygit-git recovery_ (44 passed)
  • cargo test --locked -p bitbygit-git recovery_platform_ (5 passed on Linux)
  • cargo test --locked --workspace (all workspace tests passed)
  • cargo build --locked --workspace

macOS and Windows recovery-platform jobs are left to PR CI. No unresolved local risks found.

@cosentinode

Copy link
Copy Markdown
Owner Author

Follow-up: develop advanced via #78 after the review-fix push and made this PR conflicting. Commit 76a39bb merges current origin/develop, preserves both the recovery platform matrix and the new installation CI jobs, and keeps the explicit Git 2.42 recovery prerequisite.

Post-merge verification:

  • bash scripts/test-release-workflow.sh
  • bash scripts/test-installation-docs.sh
  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (all 299 tests passed)
  • cargo build --locked --workspace

The transient pre-commit workspace test failure caused by Git detecting the in-progress merge was rerun after the merge commit and passed. No merge was performed on PR #72.

@cosentinode

Copy link
Copy Markdown
Owner Author

CI follow-up commit ea00099 addresses the first merged-base matrix run:

  • rewrites the recovery/TUI let-chains to preserve Rust 1.85 compatibility; verified locally with cargo +1.85.0 build --locked --release -p bitbygit
  • updates the diagnostic output-bound fixture for the 64 MiB diagnostic cap and accepts either the output cap or deadline as the terminating hard bound on macOS
  • adds Windows coverage proving a spawned mutating recovery command survives both deadline expiry and 5 MiB output while retaining only 32 bytes

Reverification: format, clippy with warnings denied, all 44 recovery tests, and all 299 workspace tests pass locally. Awaiting the rerun macOS/Windows matrix.

@cosentinode

Copy link
Copy Markdown
Owner Author

Final verification update: CI run https://github.com/cosentinode/bitbygit/actions/runs/29615520789 is fully green, including macOS and Windows recovery safety and all Rust 1.85 target builds. PR #72 is mergeable with CLEAN merge state at ea00099; it remains open and unmerged for the orchestrator.

@cosentinode

Copy link
Copy Markdown
Owner Author

Fresh independent review of current head ea00099 found one blocking concern:

  1. High: same-path ignored-file changes are no longer revalidated and recovery can silently overwrite them. recovery_state_until_with now fingerprints only the output/path set from git ls-files --others --ignored; it does not hash or retain handles for ignored-file contents. The new test explicitly confirms that changing an ignored file from 64 MiB to 128 MiB leaves RecoveryState equal (lines 5391-5407), while the collision regression was narrowed from a same-path content change to a path rename (lines 5293-5346). In that fixture, target/victim.bin is ignored in the paused worktree but tracked in the topic commit restored by rebase --abort. I restored the original same-path mutation (at preview -> changed after preview) against this head: recover_exact returned success and rebase --abort replaced the post-preview bytes with committed. This violates issue [Phase 12] Add recovery plans and enforce conflict-mode policy #55's state-change requirement and causes silent local data loss. Fingerprint bounded ignored contents again, or conservatively block recovery when an ignored path can collide with recovery output, and restore the same-path data-loss regression.

Verification at ea00099: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo test --locked --workspace (299 passed), and git diff --check pass. Hosted Rust, macOS/Windows recovery safety, source-build, installation-doc, and GitGuardian checks are green; none exercises this same-path ignored-content mutation.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed the ignored-file overwrite finding in commit 5891bf9.

The recovery fingerprint now hashes ignored worktree file contents with the existing race/identity fence, does not follow ignored symlinks, and uses a separate aggregate 64 MiB budget. If ignored data exceeds that bound, recovery planning fails closed instead of allowing an unverified abort.

The rebase-abort regression again mutates the same ignored collision path after preview (target/victim.bin: at preview -> changed after preview). recover_exact now returns the expected state changed after preview error, preserves the changed bytes, and leaves the rebase active. The oversized-ignored-data regression verifies the bounded fail-closed path.

Local verification:

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked -p bitbygit-git exact_rebase_abort_rejects_changed_ignored_collision_path (1 passed)
  • cargo test --locked -p bitbygit-git recovery_fingerprint_bounds_ignored_data (1 passed)
  • cargo test --locked -p bitbygit-git recovery_ (44 passed)
  • cargo test --locked -p bitbygit-git recovery_platform_ (5 Linux platform checks passed)
  • cargo test --locked --workspace (299 passed)
  • cargo build --locked --workspace
  • git diff --check

Hosted macOS and Windows recovery-safety checks are running for 5891bf9.

@cosentinode

Copy link
Copy Markdown
Owner Author

Final hosted verification for 5891bf9 is green: Rust workspace CI, Rust 1.85 source builds on Linux, Intel/macOS, Apple Silicon/macOS, and Windows, recovery safety on macOS and Windows, installation documentation checks, and GitGuardian all passed.

@cosentinode cosentinode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh independent review of current head 5891bf991bf27de86e5f1a3e0fd167c932d9ac36 found two blocking concerns:

  1. High: the process-safety check still validates a different filesystem state from the fingerprint and can be raced to launch an executable hook. recovery_state_until_with hashes and retains the hooks input, but RecoveryInputFence::validate runs the independent pathname-based process-safety scan first and only afterwards validates the recorded probes/handles. A writer can keep a non-executable hooks directory at the path for the initial scan, swap in an executable-hook directory while the snapshot records it, swap the safe directory back only for ensure_recovery_process_configuration_safe_until, and restore the same unsafe directory before input identity validation. The fingerprint and retained handle then match the unsafe directory, while the safety scan passed against the safe one. Repeating that sequence during preview and execution lets recover_exact spawn Git with the executable hook after uppercase-Y confirmation. The new test only enables a hook and leaves it enabled, so the safety scan detects it; it does not exercise alternating safe/unsafe path identities. Evaluate process safety from the exact retained observations used to construct/validate the fingerprint (or otherwise couple safety and identity atomically), and add a synchronized directory-swap regression proving no hook starts.

  2. High: fingerprinting every ignored artifact makes recovery unavailable after ordinary builds, including in this repository. The latest change enumerates every ignored path, reads every ignored regular file, and imposes both the generic retained-output/100,000-entry limits and a 64 MiB aggregate content limit (ignored loop, path-list rejection). After running this PR's required cargo test --locked --workspace in a clean worktree, ignored target/ is 641,842,755 bytes and git ls-files --others --ignored --exclude-standard --full-name -z -- emits 209,653 bytes. Recovery therefore fails before producing a plan, first on the 64 KiB retained path-list limit and, absent that, on the 64 MiB content limit. Common target, node_modules, build caches, and generated trees make the feature unusable in normal repositories. Guard only ignored paths that can actually be overwritten by the selected recovery, or use a scalable policy that preserves same-path collision protection without whole-repository caps; add a production recovery test with a realistic ignored build tree.

Verification at 5891bf9: cargo fmt --all -- --check, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo test --locked --workspace (299 passed), and git diff --check pass. All hosted checks are green and the PR is mergeable; neither case above is covered.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed both concerns from review #72 (review) in dab7f21 (fix: scope recovery safety fences).

  • Executable-hook policy is now evaluated from the same descriptor-backed metadata observation used by the recovery fingerprint. An unsafe hook directory captured by the fingerprint is rejected even if a racing writer presents a safe pathname to the independent safety scan; a safe captured directory must still pass the retained identity fence immediately before spawn. Direct and exact recovery both run the action-scoped fenced state at the final spawn boundary. The synchronized safe/unsafe directory-swap regression verifies the retained unsafe observation is rejected and the hook marker process never starts.
  • Whole-repository ignored enumeration/content hashing is removed. Recovery now derives bounded candidate paths from the selected recovery target, queries ignored status only for those literal paths, and retains both queries as final-fence probes. Any relevant ignored collision blocks before preview rather than permitting an overwrite. A rebase-abort regression proves the colliding ignored bytes are preserved, while a real exact merge abort succeeds with 4,000 ignored build artifacts and a 128 MiB ignored artifact, beyond both former limits.

Verification passed:

  • cargo fmt --all -- --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (299 passed)
  • cargo build --locked --workspace
  • cargo test --locked -p bitbygit-git recovery_ (43 passed)
  • cargo test --locked -p bitbygit-git recovery_platform_ (5 Linux platform checks passed)
  • focused ignored-collision, large-build-tree, and synchronized hook-swap regressions
  • cargo clippy --locked -p bitbygit-git --all-targets --target x86_64-pc-windows-gnu -- -D warnings
  • cargo check --locked -p bitbygit-git --all-targets --target aarch64-apple-darwin
  • cargo +1.85.0 build --locked --release -p bitbygit --target x86_64-unknown-linux-gnu
  • bash scripts/test-release-workflow.sh
  • bash scripts/test-installation-docs.sh
  • git diff --check

actionlint is not installed locally; hosted workflow checks will cover it. No merge performed.

@cosentinode

Copy link
Copy Markdown
Owner Author

Hosted verification for dab7f21 is fully green: Rust workspace CI, Rust 1.85 source builds on Linux, Intel/macOS, Apple Silicon/macOS, and Windows, recovery safety on macOS and Windows, installation documentation checks, and GitGuardian all passed. PR remains unmerged.

@cosentinode cosentinode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh independent review of the complete develop...dab7f21 diff found three blocking concerns:

  1. High: the ignored-collision candidate set misses intermediate rebase writes and recovery can delete ignored data. ensure_no_ignored_recovery_collisions_until checks only paths in the net git diff HEAD <orig-head>, and every rebase action uses the final original head as that target (lines 1807-1830). Remaining commits can touch a path and later revert it, so that path is absent from the net diff even though rebase --continue writes it. I reproduced this with a paused rebase whose remaining commits add then delete ignored victim.txt: the candidate diff listed only an unrelated upstream path, the literal ignored query reported victim.txt, planning would not query it, and git rebase --continue completed with the local ignored file deleted. Derive collision paths from every tree/patch that continue/skip can install, or conservatively block relevant ignored paths, and add an intermediate-commit data-loss regression.

  2. High: the final input fence still permits a post-preview index/control mutation before spawn. RecoveryInputFence::validate revalidates retained inputs once in sequence. After an early input such as the index is checked, validation continues through later control trees, hooks, attributes, and untracked paths; there is no recheck of that index before the callback returns and run_bounded_command spawns Git. An external git add synchronized after the index's turn therefore leaves the computed fingerprint equal to the preview and is committed by merge/rebase continue. The current synchronized test mutates after the initial index read, before validate checks retained identities, so it does not exercise this window. Recheck/fence consumed inputs at the actual spawn boundary and add synchronization after the index has passed its final-loop check.

  3. High: non-hook process-safety checks still inspect a different state from the retained fingerprint. RecoveryInputFence::validate runs the independent pathname/config safety scan first, then validates config probes and retained control-file observations. The latest fix evaluates executable-hook metadata from the hashed observation, but there is no equivalent semantic validation for core.fsmonitor, external drivers, signing config, persisted strategy/signing files, or rebase todo exec entries (safety scan, hook-only observation check). A racing writer can present safe config/control paths during each safety scan, restore the same unsafe state captured by the preview before probe/identity validation, and leave it active for spawn; the expected fingerprint then explicitly matches the unsafe state and Git can launch an uncontained helper. Evaluate all process-safety policy from the exact retained observations used by the fingerprint, with synchronized safe/unsafe config and todo/strategy swaps.

Verification at dab7f21: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo test --locked --workspace (299 passed), git diff --check, and the ignored-path reproduction above. Hosted Linux, macOS, Windows, Rust 1.85 source-build, installation-doc, and GitGuardian checks are green; these cases are not covered.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed every concern from review #72 (review) in cd9f6a0.

Fixes

  • Intermediate rebase writes: collision planning now parses the exact retained rebase todo, fingerprints every remaining replay commit's touched paths, and checks ignored collisions against their union plus the end target. The add-then-delete regression demonstrates that victim.txt is blocked even though it is absent from the net HEAD...orig-head diff and verifies the local ignored contents remain intact.
  • Final spawn fence: retained probes/paths are validated twice inside run_bounded_command's immediate pre-spawn callback. The application lock check and synchronized test hook run between passes, and the retained index is deliberately checked last on the final pass. The post-first-pass git add regression verifies that HEAD is unchanged and recovery remains active.
  • Exact process-safety observations: fsmonitor/signing/external-driver policy is evaluated directly from config probe bytes/status that are hashed and retained; core.hooksPath selection is retained too. Rebase todo, signing, strategy, and strategy-option policy is evaluated while hashing the exact retained file contents. Diagnostic fingerprint commands force core.fsmonitor=false so a raced config cannot launch it during inspection. Synchronized fsmonitor, todo, and strategy mutations after the first final pass all fail closed, and direct exact-observation regressions cover unsafe todo/strategy contents.

Verification

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (303 passed)
  • cargo test --locked -p bitbygit-git recovery (46 passed)
  • cargo test --locked -p bitbygit-git recovery_platform_ (5 passed on Linux)
  • cargo build --locked --workspace
  • cargo +1.85.0 build --locked --release -p bitbygit --target x86_64-unknown-linux-gnu
  • bash scripts/test-release-workflow.sh
  • bash scripts/test-installation-docs.sh
  • git diff --check

Hosted macOS and Windows recovery/build checks are left to the PR CI matrix. No merge performed.

@cosentinode

Copy link
Copy Markdown
Owner Author

Hosted verification for cd9f6a0 is now fully green: Rust workspace CI, Linux/macOS Intel/macOS ARM/Windows Rust 1.85 source builds, macOS and Windows recovery safety, macOS and Windows installation docs, and GitGuardian all passed. CI run: https://github.com/cosentinode/bitbygit/actions/runs/29618142096

@cosentinode cosentinode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh independent review of the complete current develop...cd9f6a0 diff found four blocking concerns:

  1. High: abort recovery can overwrite ignored local data on paths omitted from the endpoint diff. ensure_no_ignored_recovery_collisions_until seeds collision candidates from git diff --name-only HEAD <abort-target>, while merge abort selects ORIG_HEAD (lines 1925-1934). In a normal conflicted merge HEAD == ORIG_HEAD, so that candidate set is empty. I reproduced this at the public prepare_recovery/recover_exact path: start a conflict with tracked stable.txt unchanged on both branches, run git rm --cached stable.txt, ignore stable.txt, and replace its worktree contents with local data. Planning succeeds because ordinary untracked enumeration excludes ignored paths and the endpoint diff does not name stable.txt; confirmed merge abort then restores the tracked blob over the local data. The same endpoint-net-diff gap applies to unchanged target paths during rebase abort. Collision planning must cover every target path the abort checkout can overwrite, with a regression proving ignored contents remain intact.

  2. High: a standard global Git LFS installation disables every recovery, while the test configuration hides this production behavior. Recovery rejects every configured filter.*.(clean|smudge|process) key regardless of whether any repository/target path selects that filter (lines 1595-1625). A normal git lfs install creates global filter.lfs.clean, filter.lfs.smudge, and filter.lfs.process, so even a non-LFS repository fails planning with “configured external Git drivers...” and none of issue #55's actions are usable. I reproduced that through production Git::new; the suite misses it because constructors set isolated_test_config: cfg!(test) (lines 134-168) and replace global config for all tests. Check effective attributes/actual activation rather than mere global key presence, or explicitly narrow and test/document this substantial compatibility limit with a non-isolated production-path test.

  3. Medium: the configured 60-second execution deadline does not apply after the mutating Git process starts. recover/recover_exact calculate recovery_execution_timeout (lines 381-484), but run_bounded_command explicitly skips its deadline whenever the policy is RecoveryExecution (lines 2805-2835); tests deliberately assert that behavior (lines 5784-5806). A stalled Git/filesystem operation or nonterminating output therefore blocks the synchronous TUI and holds the application recovery lock indefinitely. Either enforce a safe execution bound/termination strategy or remove the false timeout contract and make execution cancellable without leaving the UI permanently wedged.

  4. Blocking integration: the PR is currently unmergeable with develop. GitHub reports mergeable: CONFLICTING; git merge-tree --write-tree origin/develop cd9f6a0 confirms a content conflict in crates/bitbygit-tui/src/lib.rs after develop advanced to 62ae924. The green checks ran against the prior base 32c0cb8, so they do not verify the required conflict resolution or combined recovery/default-PR-base behavior. Update from current develop, resolve the overlap, and rerun the full matrix.

Verification at cd9f6a0: cargo test --locked --workspace passes (303 tests), git diff --check passes, current hosted checks are green, the ignored-data overwrite was reproduced through recover_exact, the global Git LFS rejection was reproduced through production Git::new, and the current merge conflict was reproduced with git merge-tree.

@cosentinode

Copy link
Copy Markdown
Owner Author

Addressed all findings from review 4726642751 in 2e6097b and f4ffe98.

Changes:

  • Incorporated current origin/develop non-destructively in merge commit 2e6097b, preserving both conflict-mode recovery validation and the new typed/default PR target behavior.
  • Abort recovery now checks ignored collisions against every tracked path in the restore target tree, rather than only the endpoint net diff. Regression: merge_abort_blocks_ignored_collision_omitted_from_endpoint_diff reproduces the unchanged ORIG_HEAD path case and proves planning aborts while local ignored contents and merge state remain intact.
  • Configured filter drivers now block only when effective attributes select them in the current state, abort target, or remaining rebase trees. Unrelated global Git LFS config is accepted by recovery_allows_inactive_global_lfs_configuration; active current/target filters remain fail-closed, including rebase_abort_rejects_filter_selected_only_by_target_tree and the spawn-boundary race regression exact_recovery_rejects_filter_activated_at_spawn_boundary. Other external driver classes remain blocked.
  • RecoveryExecution now enforces the configured deadline after spawn and terminates the contained process tree. Regressions cover direct post-spawn timeout and descendant termination.

Verification (all passed locally on Linux):

  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked -p bitbygit-git (113 tests)
  • cargo test --locked -p bitbygit-git recovery_platform_ (4 tests)
  • cargo test --locked --workspace (321 tests)
  • cargo build --locked --workspace
  • cargo build --locked --release -p bitbygit
  • isolated cargo install --locked --path crates/bitbygit and installed bitbygit --version smoke (bitbygit 0.1.0)
  • bash scripts/test-release-workflow.sh
  • bash scripts/test-installation-docs.sh
  • git diff --check

Hosted macOS/Windows recovery and supported-target source-build checks are pending on the pushed commits. The PR remains unmerged.

@cosentinode

Copy link
Copy Markdown
Owner Author

Hosted verification for f4ffe98 is complete: all PR checks pass, including Rust/clippy/fmt/workspace tests, macOS and Windows recovery safety, macOS and Windows installation documentation, GitGuardian, and Rust 1.85 release builds for x86_64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin, and x86_64-pc-windows-msvc. GitHub reports the PR mergeable with the current develop base. The PR remains unmerged for the required fresh-review loop.

@cosentinode cosentinode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh independent review findings at f4ffe98

  1. High: the fixed execution deadline can SIGKILL Git after destructive recovery has begun, leaving a partially rewritten repository. recover_exact_with starts one 60-second deadline before all execution-time checks, and run_bounded_command kills the process group whenever that deadline expires after spawn. The final fingerprint can consume most of the budget, so a valid recovery may start with almost no execution time left. Merge/rebase continue, skip, and abort update the worktree/index incrementally; SIGKILL is not transactional. I reproduced the underlying Git behavior with a 20,000-file repository: killing git reset --hard after 20 ms left the old HEAD, 881 dirty paths containing a partial rewrite, and .git/index.lock. The added timeout test only verifies that a synthetic child returns TimedOut; it never checks repository consistency. Do not force-kill Git after a mutating recovery starts (or provide a recovery-specific cancellation design that can prove consistency), and add an interrupted real-recovery regression that verifies no partial state or stale lock remains.

  2. Medium: release installation requirements still claim any Git version works, while every recovery action rejects Git older than 2.42. The shipped installation guide lists only “git ... available on PATH” as a runtime prerequisite (docs/installation.md:8-13), but recovery unconditionally rejects older Git (crates/bitbygit-git/src/lib.rs:3206-3223). The README change is under development prerequisites, so archive users on stock Ubuntu 22.04 (Git 2.34) or Debian 12 (2.39) are still told they satisfy runtime requirements and only discover that issue #55 is unavailable after attempting recovery. Document the feature-specific minimum in the runtime installation instructions and validate that statement in the installation-doc tests.

  3. Medium: the macOS/Windows recovery job does not execute a real recovery action. The new matrix runs only cargo test --locked -p bitbygit-git recovery_platform_ (.github/workflows/ci.yml:86-107). That filter selects capability/lock primitives (four tests on Linux), not merge_abort_clears_operation_and_restores_head, rebase continue/abort/skip, or the TUI five-action integration test. All actual recovery success coverage therefore runs only in the Linux workspace job despite platform-specific process groups, Windows Job Objects, file sharing, and identity handling. Run at least one real merge and rebase recovery through the public exact-recovery path on each shipped platform.

Verification: git diff --check, cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, and cargo test --locked --workspace all pass locally. All current hosted checks are green; the timeout consistency case and cross-platform end-to-end actions are not covered.

@cosentinode

Copy link
Copy Markdown
Owner Author

Review fixes pushed

Commit: 9c0feaa (fix: avoid interrupting mutating recovery)

Addresses the full review at #pullrequestreview-4726769095:

  • Recovery uses a 60-second start deadline for version/config/state/fence checks and the final pre-spawn boundary. After mutating Git is spawned, BitByGit waits for Git to finish instead of timeout-killing it. Output remains memory-bounded and streamed, and contained descendants are still terminated after Git exits so inherited pipes cannot leak.
  • Added a real 2,000-path git merge --abort regression that crosses the start deadline and verifies restored HEAD/content, a clean worktree, and no stale index.lock.
  • Added public recover_exact end-to-end merge-abort and rebase-abort coverage, including post-run index.lock checks and BitByGit lock reacquisition.
  • Kept the broad recovery_platform_ macOS/Windows CI selector so existing output/lock tests remain covered; it now also selects real merge/rebase execution and Unix process-group/Windows Job Object cleanup tests. scripts/test-ci-workflow.sh validates the job, matrix, selector, and named tests.
  • Documented the Git 2.42 guarded-recovery minimum under runtime installation prerequisites and asserted it in both Bash and PowerShell installation validators.

Local verification passed:

  • git diff --check
  • cargo fmt --all --check
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace (321 unit tests plus doc tests)
  • cargo test --locked -p bitbygit-git recovery_ (48 tests)
  • cargo test --locked -p bitbygit-git recovery_platform_ (6 applicable Linux tests)
  • cargo build --locked --workspace
  • cargo check --locked -p bitbygit-git --all-targets --target aarch64-apple-darwin
  • cargo check --locked -p bitbygit-git --all-targets --target x86_64-pc-windows-msvc
  • cargo check --locked -p bitbygit-git --all-targets --target x86_64-pc-windows-gnu
  • workspace cfg checks for aarch64-apple-darwin and x86_64-pc-windows-msvc
  • pinned actionlint 1.7.12
  • bash scripts/test-ci-workflow.sh
  • bash scripts/test-release-workflow.sh
  • bash scripts/test-installation-docs.sh

Hosted macOS/Windows execution and the Windows PowerShell docs validator are running in CI. A broader cross-target workspace --all-targets check remains blocked by pre-existing Unix-only TUI test code; the changed bitbygit-git test target compiles cleanly for Apple ARM and both installed Windows toolchains.

@cosentinode

Copy link
Copy Markdown
Owner Author

Windows CI follow-up: commit f47a1bc makes the large merge-abort fixture deterministic with repository-local core.autocrlf=false. The failed run showed that all recovery behavior completed successfully and only the strict content assertion observed Git-for-Windows CRLF conversion (main\r\n versus main\n). The fixture now preserves the strict byte assertion without normalizing away differences.

Reverified locally:

  • cargo test --locked -p bitbygit-git recovery_platform_end_to_end_deadline_does_not_interrupt_merge_abort
  • cargo clippy --locked -p bitbygit-git --all-targets -- -D warnings
  • cargo check --locked -p bitbygit-git --all-targets --target x86_64-pc-windows-msvc
  • cargo fmt --all --check
  • git diff --check

A new hosted run is in progress.

@cosentinode

Copy link
Copy Markdown
Owner Author

Final hosted verification is green at head f47a1bc: https://github.com/cosentinode/bitbygit/actions/runs/29620216647

Passed checks include:

  • Recovery safety on macos-latest (real deadline regression, exact merge/rebase recovery, output bounding, process-group cleanup, and lock coverage)
  • Recovery safety on windows-latest (real deadline regression, exact merge/rebase recovery, output bounding, Windows Job Object cleanup, and lock coverage)
  • Rust workspace format/clippy/test/build job
  • Rust 1.85 release builds for Linux x86-64, macOS Intel, macOS Apple silicon, and Windows x86-64
  • macOS and Windows installation documentation validators
  • GitGuardian

PR #72 is open and mergeable. No merge was performed.

@cosentinode cosentinode left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fresh independent review of the complete current develop...f47a1bc diff found two blocking concerns:

  1. High: rebase --skip can overwrite ignored local data on paths unchanged between HEAD and the original branch. For every non-abort rebase action, collision candidates start with git diff --name-only HEAD <orig-head> (lines 1919-1944), while recovery_overwrite_target_until treats skip exactly like continue (lines 2106-2141). But skip first resets the conflicted index/worktree to the current HEAD; any path tracked identically by HEAD and the original branch is absent from that diff even though the reset writes it. I reproduced this through public prepare_recovery/recover_exact: during a one-commit conflict, remove an otherwise unchanged tracked stable.txt from the index, ignore it, and replace it with local data. Planning succeeds and confirmed skip replaces the local data with the tracked blob. Include every path the skip reset can overwrite (as well as future commits), and add a regression asserting ignored contents survive.

  2. High: apply-backend rebases omit all remaining patches from collision and external-process checks. remaining_rebase_commit_oids_until only reads rebase-{merge,apply}/git-rebase-todo and returns an empty set when those files are absent (lines 2087-2104); a real git rebase --apply stores numbered patch files plus next/last, not a todo file. Both future collision discovery (lines 1984-2025) and future filter activation checks (lines 1733-1749) therefore see no remaining work. I reproduced a three-commit apply rebase where an intermediate commit added an ignored victim.txt and .gitattributes, and the next removed both so the endpoint diff was empty. Public exact continue accepted the preview, deleted the local ignored file, and launched the configured smudge filter selected only by that intermediate attributes patch. Either model every remaining apply patch safely for both paths and attributes, or fail closed on apply-backend continue/skip; cover both data preservation and process non-execution.

Verification at f47a1bc: cargo fmt --all --check, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo test --locked --workspace (321 passed), bash scripts/test-ci-workflow.sh, and git diff --check pass. All current hosted Linux, macOS, Windows, Rust 1.85 source-build, installation-doc, and GitGuardian checks are green; the two public-API repros above fail and are not covered.

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.

[Phase 12] Add recovery plans and enforce conflict-mode policy

1 participant