Skip to content

Add review-patch command for interactive patch inspection#1290

Open
spoorcc wants to merge 18 commits into
mainfrom
claude/review-patch-command-xyosoo
Open

Add review-patch command for interactive patch inspection#1290
spoorcc wants to merge 18 commits into
mainfrom
claude/review-patch-command-xyosoo

Conversation

@spoorcc

@spoorcc spoorcc commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Introduces dfetch review-patch which stages the clean upstream source in
the git index and applies the selected patches to the working tree, so any
diff-aware editor sees git diff (working tree vs index) showing exactly
what the patches contribute. The command always restores original state on
exit — no permanent changes to working tree or index.

review-patch demo

  • New command: dfetch/commands/review_patch.py with --count/-n, --interactive/-i
  • GitSuperProject.add_path(), restore_staged(), restore_worktree() for index/worktree control
  • GitLocalRepo.add_path(), restore_staged(), restore_worktree() with -- pathspec hardening
  • SVN superprojects supported (with a warning; no staging step; uses svn diff hint)
  • Interactive TUI uses read_key()/Screen for ← → step-through (zero fetches per step)
  • Only one fetch per review session (clean upstream); patches applied/reversed directly
  • --count validated >= 0; reported count clamped to min(requested, total)
  • 9 unit tests, 4 git BDD scenarios
  • Threat model updated with review-patch and transient git index mutation note
  • Documentation and changelog updated

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF

Summary by CodeRabbit

  • New Features

    • Added review-patch command to interactively inspect patch contributions without permanent changes
    • Supports limiting reviewed patches with --count and step-by-step mode with --interactive
    • Works with Git and SVN superprojects
  • Documentation

    • Added comprehensive patching workflow guide with practical examples and demo recordings
    • Included automated test coverage for new functionality

Introduces `dfetch review-patch` which stages the clean upstream source in
the git index and applies the selected patches to the working tree, so any
diff-aware editor sees `git diff` (working tree vs index) showing exactly
what the patches contribute. The command always restores original state on
exit — no permanent changes to working tree or index.

- New command: dfetch/commands/review_patch.py with --count/-n, --interactive/-i
- GitSuperProject.add_path() and restore_staged() methods for index control
- GitLocalRepo.add_path() and restore_staged() as git-level primitives
- SVN superprojects supported (with a warning; no staging step)
- Interactive TUI uses read_key()/Screen for ← → step-through
- 8 unit tests, 4 git BDD scenarios
- Documentation and changelog updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b5f8c33-d2c8-4b56-b8da-43ff04ac9844

📥 Commits

Reviewing files that changed from the base of the PR and between 6be3991 and cfc9798.

📒 Files selected for processing (2)
  • dfetch/commands/review_patch.py
  • dfetch/vcs/git.py

Walkthrough

Adds a new dfetch review-patch CLI command that stages clean upstream sources in Git, applies patch stacks to the working tree for diff-based inspection, and optionally steps through patches interactively via a TUI. Extracts shared Command._iter_projects iteration logic, adds Git index operations (add_path, restore_staged, restore_worktree), exposes SubProject.apply_patches, and includes unit tests, BDD scenarios, documentation, CI integration, and compliance documentation regeneration.

Changes

review-patch command feature

Layer / File(s) Summary
Shared Command._iter_projects helper and UpdatePatch refactor
dfetch/commands/command.py, dfetch/commands/update_patch.py
Adds Command._iter_projects static method that runs a per-project callback inside superproject.root_directory, logs RuntimeError per project, and re-raises if any failed. UpdatePatch.__call__ is refactored to delegate to _iter_projects, removing manual error tracking.
Git index operations: add_path, restore_staged, restore_worktree
dfetch/vcs/git.py, dfetch/project/gitsuperproject.py
Adds three public methods to GitLocalRepo wrapping git add, git restore --staged, and git restore for path-scoped operations. GitSuperProject gains matching forwarding methods.
SubProject.apply_patches public method
dfetch/project/subproject.py
Exposes apply_patches(count: int = -1) as public API on SubProject, delegating to the existing internal _apply_patches helper.
ReviewPatch command: CLI, review flow, and interactive TUI
dfetch/commands/review_patch.py
Implements ReviewPatch with CLI arg parsing (--count/--interactive), TTY validation, per-project skip conditions (no patch, never fetched, local changes), clean-upstream update(patch_count=0), Git add_path staging, bulk apply or arrow-key TUI (_step_tui/_apply_step/_draw_tui_frame), and guaranteed try/finally restoration via restore_staged for Git or full update for SVN.
CLI registration
dfetch/__main__.py
Imports dfetch.commands.review_patch and registers ReviewPatch.create_menu(subparsers) in create_parser().
Unit tests for ReviewPatch
tests/test_review_patch.py
Covers Git happy-path (all patches, count=1), non-Git warning and skip-staging, three skip scenarios (no patches, never fetched, local changes), and three error conditions (no VCS superproject, interactive without TTY, negative count).
BDD feature scenarios and Behave step helpers
features/review-patch-in-git.feature, features/review-patch-in-svn.feature, features/steps/git_steps.py
Adds Gherkin scenarios for Git (apply all, --count 0, no patch defined, uncommitted changes) and SVN (apply all, --count 0) superprojects. Adds a @then step asserting git status --porcelain reports no changes.
Documentation, demo, and changelog
doc/howto/patching.rst, CHANGELOG.rst, doc/generate-casts/review-patch-demo.sh, doc/asciicasts/review-patch.cast, doc/generate-casts/generate-casts.sh
Adds "Reviewing a patch" section covering usage, Git staging behavior, --count/--interactive options, and restoration. Updates changelog. Creates demo shell script and asciicast recording.
CI integration
.github/workflows/run.yml
Adds dfetch review-patch step after dfetch update-patch in both test-cygwin and matrix run jobs.

Auto-generated documentation and security metadata updates

Layer / File(s) Summary
Compliance tracking and control register regeneration
doc/explanation/compliance_track.rst, doc/explanation/control_register.rst
Regenerates CRA Annex I compliance traceability tables with updated SO sub-row formatting for ECR-B through ECR-M; applies explicit row separators to match generator output.
Security metadata and threat model updates
security/dfetch.component-definition.json, security/tm_supply_chain.py, security/tm_usage.py, doc/_ext/latex_tabs.py
Fixes JSON closing brace; reformats imports to multi-line parenthesized form; expands dfetch_cli process description to include new CLI subcommands and documents potential git-index consistency risk on interrupted review-patch.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as ReviewPatch.__call__
    participant SuperRepo as GitSuperProject
    participant SubRepo as SubProject
    participant TUI as _step_tui

    User->>CLI: dfetch review-patch [--count N | -i] [projects]
    CLI->>CLI: validate args (TTY check, count >= 0)
    CLI->>SuperRepo: create superproject, check VCS type
    loop each selected project
        CLI->>SubRepo: create_subproject(project)
        CLI->>CLI: _can_review_project (patch/fetch/changes checks)
        alt skip condition met
            CLI-->>User: log warning, skip project
        else eligible for review
            CLI->>SubRepo: update(patch_count=0)
            CLI->>SuperRepo: add_path(project_path)
            alt --interactive
                CLI->>TUI: _step_tui(patches)
                TUI-->>User: _draw_tui_frame + key hints
                User-->>TUI: LEFT / RIGHT / ENTER / ESC
                TUI-->>CLI: complete or abort
            else bulk apply
                CLI->>SubRepo: apply_patches(count)
                CLI-->>User: "inspect via git diff / svn diff"
            end
            Note over CLI,SuperRepo: try/finally guaranteed restoration
            CLI->>SuperRepo: restore_staged or restore_from_head
            CLI->>SubRepo: update(patch_count=original)
            CLI-->>User: "Restored original state"
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • dfetch-org/dfetch#944: This PR extends the patch-application machinery (e.g., patch_count parameter, _apply_patches) introduced by the update-patch PR by adding a public SubProject.apply_patches() wrapper and the full review-patch command.
  • dfetch-org/dfetch#999: This PR's new SubProject.apply_patches() delegates to _apply_patches(), which was refactored in the retrieved PR to use the Patch.from_file(...).apply(...) API.

Suggested labels

enhancement, testing

Suggested reviewers

  • ben-edna
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add review-patch command for interactive patch inspection' directly summarizes the main change—introducing a new review-patch command with interactive capabilities for inspecting patches, which aligns perfectly with the changeset's primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/review-patch-command-xyosoo

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread dfetch/commands/review_patch.py Fixed
Comment thread tests/test_review_patch.py Fixed
- Extract shared project-iteration loop into Command._iter_projects to
  eliminate duplicate code between review_patch and update_patch (R0801)
- Fix import order: review_patch before update in __main__.py (isort)
- Remove redundant `import dfetch.project` in review_patch; import
  create_sub_project directly instead (code review finding)
- Remove unused MagicMock import from test_review_patch (code review finding)
- Update test patch targets to match new import locations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dfetch/commands/review_patch.py`:
- Around line 113-200: The `_review_project` method has too many branches and
conditional paths, exceeding the cyclomatic complexity limit of 8. Extract the
guard validations (checking for patch existence, on_disk_version, and local
changes) into a separate helper method that returns early if validation fails,
move the interactive review logic and non-interactive logic into their own
helper methods, and simplify the main method to orchestrate validation,
application, and restoration in a clearer sequence. This will distribute the
branching logic across focused helper methods while keeping the main method as a
clear orchestrator.
- Around line 70-78: The --count argument lacks validation, allowing negative
integers that produce unexpected Python slice behavior rather than a meaningful
CLI contract. Add a custom type validator to the add_argument call for --count
to ensure only positive integers are accepted. Additionally, update the logic
around line 173 where the raw count is forwarded to patch_count to validate
against negative values and clamp to valid ranges. Finally, modify the reporting
logic around line 181 to track and report the actual number of patches that were
successfully applied, not the requested count, since the effective count may
differ from what was requested.
- Around line 183-187: The logger.print_info_line call in the review_patch
function currently hardcodes the instruction to use git diff, but this is
inconsistent with non-Git superprojects like SVN that should use their own diff
commands. Make the diff command suggestion in the message VCS-aware by checking
the project's VCS type and conditionally including the appropriate diff command
(git diff for Git projects, svn diff for SVN projects, etc.) in the status
message printed to the user.

In `@dfetch/vcs/git.py`:
- Around line 785-793: Add the `--` separator before the path argument in both
the add_path and restore_staged methods to prevent Git from interpreting
option-style paths as flags. In add_path, insert `"--"` between `"add"` and
`path` in the command list passed to run_on_cmdline. In restore_staged, insert
`"--"` between `"--staged"` and `path` in the command list passed to
run_on_cmdline. This ensures Git treats the path as a positional argument rather
than a potential option flag.

In `@tests/test_review_patch.py`:
- Around line 79-97: Add a new test function to validate that negative count
values are rejected by the ReviewPatch command. Create a test that instantiates
ReviewPatch, mocks the required dependencies (create_super_project,
create_sub_project, in_directory, is_tty) similar to
test_review_count_1_uses_patch_count_1, and then calls cmd(_make_args(count=-1))
while asserting that this raises an appropriate validation error or exception.
This ensures the CLI contract maintains count-based validation and prevents
regression to slice-driven behavior for negative values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1290abec-833f-46ef-9d9d-5ff586b6111b

📥 Commits

Reviewing files that changed from the base of the PR and between 9aea63f and 0ec01e1.

📒 Files selected for processing (11)
  • CHANGELOG.rst
  • dfetch/__main__.py
  • dfetch/commands/command.py
  • dfetch/commands/review_patch.py
  • dfetch/commands/update_patch.py
  • dfetch/project/gitsuperproject.py
  • dfetch/vcs/git.py
  • doc/howto/patching.rst
  • features/review-patch-in-git.feature
  • features/review-patch-in-svn.feature
  • tests/test_review_patch.py

Comment thread dfetch/commands/review_patch.py
Comment thread dfetch/commands/review_patch.py Outdated
Comment thread dfetch/commands/review_patch.py Outdated
Comment thread dfetch/vcs/git.py Outdated
Comment thread tests/test_review_patch.py
- Add `dfetch review-patch` step after `dfetch update-patch` in both
  the `run` (all platforms) and `test-cygwin` jobs in run.yml
- Add `review-patch-demo.sh` cast generation script (mirrors
  update-patch-demo.sh; pipes stdin to avoid blocking on input())
- Register the demo in generate-casts.sh after the update-patch line
- Add synthetic review-patch.cast (asciicast v2) showing the full
  flow: manifest inspection, patch preview, staging, and restore
- Wire `.. asciinema:: ../asciicasts/review-patch.cast` into the
  Reviewing a patch section of doc/howto/patching.rst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
In the default (all-patches) case review-patch was fetching three
times: once for clean upstream, once to apply all patches, and once
more in the finally block to "restore" — even though the working tree
was already fully patched after the second fetch.

Track worktree_fully_patched after the non-interactive update call and
skip the finally fetch when the working tree is already at patch_count=-1.
The --count N and --interactive paths still re-fetch because the final
working-tree state differs from fully patched.

Update tests and the synthetic demo cast accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
After the initial update(patch_count=0) puts clean upstream in the
working tree there is no reason to re-fetch for any subsequent step.

- Add SubProject.apply_patches(count) — applies patch files to the
  already-fetched working tree without going back to the remote.
- Add GitSuperProject.restore_worktree(path) / GitLocalRepo.restore_worktree
  — runs `git restore <path>` to reset the working tree from the staged
  index (which holds clean upstream throughout the review session).
- Non-interactive: replace the second update() call with apply_patches().
  Git all-patches case: 1 fetch total (was 2).
  Git --count N case: 1 fetch + restore_worktree + apply_patches (was 3).
- Interactive TUI: replace per-step update() calls with direct patch
  apply/reverse (Patch.from_file().apply() / .reverse().apply()).
  No fetch at all during stepping, for both Git and SVN.
- Finally block (Git): restore_worktree + apply_patches instead of a
  re-fetch. SVN still re-fetches when the worktree is not fully patched
  (no staged index to restore from).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
claude added 2 commits June 19, 2026 05:18
Extract _can_review_project, _apply_review, _restore_project,
_draw_tui_frame, and _apply_step as module-level helpers so every
function stays below the CC=8 limit enforced by CI. Black-format
tests to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
Black reformatted latex_tabs.py, tm_supply_chain.py, tm_usage.py,
and related doc/security files during the review-patch session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
After the refactor, review-patch does only one fetch (clean upstream)
and applies/reverses patches directly without re-fetching. The expected
output in the BDD scenarios previously included 3 Fetched lines; update
them to reflect the new single-fetch flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
…ff cmd

- Reject --count < 0 with a RuntimeError early in __call__
- Add test_negative_count_raises to prevent regression
- Add -- separator before path in git add, git restore --staged,
  and git restore to prevent option-style paths being parsed as flags
- Show `svn diff` in the status message for SVN superprojects instead
  of always saying `git diff`
- Clamp reported patch count to min(requested, total) so the message
  reflects patches actually applied rather than the raw CLI argument
- Add review-patch to the dfetch_cli threat-model description, noting
  the transient git index mutation and interruption risk
- Reduce _apply_review to 5 args by pre-computing chosen_count and
  info_msg in _review_project (fixes pylint too-many-arguments)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
Remove the second (now-eliminated) fetch from the cast, regenerate
with agg to produce an animated GIF showing the single-fetch flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
- Replace sp.__class__ = GitSuperProject/NoVcsSuperProject with
  Mock(spec=...) so isinstance() checks work without unsafe __class__
  mutation that pyright rejects
- Add str | None type annotation to on_disk_version parameter so
  passing None in test_never_fetched_logs_warning_and_skips is valid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
Replace is_git: bool + assert isinstance(superproject, GitSuperProject)
pattern with git_super: GitSuperProject | None so the type checker
tracks the narrowed type directly — no assert needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
update(patch_count=0) writes patch: '' to the metadata file.
The restore path fixes the working tree but apply_patches() has no
metadata-writing logic, leaving the metadata file missing the patch list.

Fix: snapshot the metadata file bytes before the clean-upstream fetch
and write them back in the finally block after restore completes.

Add a BDD step "the metadata of ... lists patch ..." and assert it in
both Git and SVN review-patch scenarios to pin the invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
Instead of asserting the patch field in dfetch_data.yaml directly,
verify from the user's VCS perspective: git status --porcelain SomeProject
must report nothing after review-patch exits, covering both the working
tree files and the metadata file in one shot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
patch_ng logs "successfully patched N/M: file" via Python logging
when Patch.apply() is called. In the TUI loop this output appears
between frames, so Screen.draw()'s cursor-up count is wrong and
the help line gets duplicated on each arrow keypress.

Suppress the patch_ng logger to CRITICAL for the duration of each
LEFT/RIGHT apply/reverse operation inside the TUI loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
If Patch.apply() or Patch.reverse().apply() raises RuntimeError during
the interactive stepper, the TUI frame was left on screen while the
error and 'restored' messages printed below it. Catch RuntimeError in
_step_tui, clear the screen, then re-raise so the existing try/finally
in _review_project still runs _restore_project (working tree and
metadata are always restored), and _iter_projects logs the error under
the project name as usual.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
The old restore path did restore_worktree (WT = staged = clean upstream)
followed by apply_patches(), which would hit the same failing patch and
raise again, leaving the index staged, the metadata corrupted, and the
working tree in the wrong state.

Replace with restore_from_head (git restore --source=HEAD --staged
--worktree -- <path>) which atomically resets both the working tree and
the index to the committed HEAD state without re-applying patches.
This makes restore unconditionally safe regardless of patch health.

Also nest the metadata write_bytes in its own try/finally so the
dfetch_data.yaml snapshot is always restored even if _restore_project
encounters an error (e.g. SVN path where apply_patches can still fail).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zY8BoH65KBX6cz7Pm8aeF
import argparse
import tempfile
from pathlib import Path
from unittest.mock import ANY, Mock, call, patch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants