Skip to content

feat: Add pu diff command to show changes across agent worktrees#85

Merged
2witstudios merged 3 commits intomainfrom
pu/diff-command
Mar 7, 2026
Merged

feat: Add pu diff command to show changes across agent worktrees#85
2witstudios merged 3 commits intomainfrom
pu/diff-command

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Mar 7, 2026

Summary

  • Adds pu diff — shows what code agents have changed across worktrees by computing git diffs against each worktree's base branch
  • Fills the biggest workflow gap: pu spawnpu statuspu diff → review → merge
  • Full-stack implementation: protocol types, engine handler, CLI command, output formatting

Usage

pu diff                  # diffs for all active worktrees
pu diff --worktree wt-X  # diff a specific worktree
pu diff --stat           # file summary instead of full diff
pu diff --json           # machine-readable output

Human-readable output shows each worktree with its branch, base, change counts, and the full diff:

Worktree fix-auth (main -> pu/fix-auth)
  3 file(s) changed, 45 insertion(s), 12 deletion(s)

diff --git a/src/auth.rs b/src/auth.rs
...

What's included

Layer Changes
Protocol (pu-core) Request::Diff, Response::DiffResult, WorktreeDiffEntry struct with error field, PROTOCOL_VERSION bumped to 2
Engine (pu-engine) handle_diff handler, diff_worktree with merge-base semantics, resolve_base_ref for stable base storage
CLI (pu-cli) pu diff command with --worktree, --stat, --json flags
Output Colored human-readable formatting + JSON mode + structured error display
Tests 18 new tests: protocol round-trips, stat parsing, worktree diff integration, output smoke tests

Review feedback addressed

All CodeRabbit review comments have been addressed:

  1. Protocol version bumpPROTOCOL_VERSION bumped from 1 → 2 for the new wire-format variants
  2. Structured error field — Added error: Option<String> to WorktreeDiffEntry so JSON clients can distinguish failures from empty diffs
  3. Missing worktree handling — Targeted queries (--worktree <id>) now report errors for missing directories; bulk queries skip gracefully
  4. Merge-base semanticsdiff_worktree uses git merge-base HEAD <base> instead of tip-of-base, showing only the worktree's own changes
  5. HEAD resolution — Worktree creation resolves HEAD to actual branch name (e.g. main) to prevent merge-base HEAD HEAD from returning the worktree's own tip

Test plan

  • All 386 tests pass (18 new + 368 existing)
  • Protocol round-trips for Diff request with defaults and explicit fields
  • WorktreeDiffEntry serialization round-trip (including new error field)
  • parse_diff_stat handles: full stat, insertions-only, deletions-only, empty
  • diff_worktree integration tests: changes present, no changes, stat mode
  • Output formatting smoke tests: with diffs, empty, no changes
  • Pre-commit hooks pass (formatting + hygiene)
  • All CI checks green (Format & Lint, Test, Dependency Audit, Build & Test, CodeRabbit)

🤖 Generated with Claude Code

After agents finish work, you need to see what code they changed.
This adds `pu diff` which computes git diffs for each worktree
against its base branch — the missing step between `pu status`
and reviewing/merging agent work.

Usage:
  pu diff              # diffs for all active worktrees
  pu diff --worktree X # diff a specific worktree
  pu diff --stat       # file summary instead of full diff
  pu diff --json       # machine-readable output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 13 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96c370ef-935c-4c2b-847f-4a67deedd9c2

📥 Commits

Reviewing files that changed from the base of the PR and between 3940e29 and e84b6f4.

📒 Files selected for processing (2)
  • crates/pu-engine/src/engine.rs
  • crates/pu-engine/src/git.rs
📝 Walkthrough

Walkthrough

Adds a new diff feature: CLI subcommand, protocol Request/Response types, engine handling to compute per-worktree diffs, and git integration to produce diff text and statistics; includes output rendering and tests.

Changes

Cohort / File(s) Summary
CLI Command & Wiring
crates/pu-cli/src/main.rs, crates/pu-cli/src/commands/mod.rs, crates/pu-cli/src/commands/diff.rs
Adds Diff subcommand (worktree, stat, json), registers pub mod diff, and implements commands::diff::run to ensure daemon, build project_root, send Request::Diff, and print the response.
CLI Output Rendering
crates/pu-cli/src/output.rs
Extends print_response to handle Response::DiffResult: per-worktree headers, branch/base display, error/no-change messages, aggregate metrics, optional diff output, and tests for various diff scenarios.
Protocol
crates/pu-core/src/protocol.rs
Bumps PROTOCOL_VERSION to 2. Adds Request::Diff { project_root, worktree_id, stat }, Response::DiffResult { diffs: Vec<WorktreeDiffEntry> }, and the WorktreeDiffEntry struct with diff text, metrics, and optional error; includes round-trip serde tests.
Engine Request Handling
crates/pu-engine/src/engine.rs
Adds Request::Diff handling and new async handle_diff that reads project manifest, selects target worktrees (specific or all active), calls git diff logic, and aggregates per-worktree DiffResult entries with per-entry errors as needed.
Git Integration & Helpers
crates/pu-engine/src/git.rs
Introduces DiffOutput struct and pub async fn diff_worktree(...) that computes diff (optionally against base via merge-base), always computes --stat for metrics, returns full diff when stat is false. Adds run_git_allow_empty helper and parse_diff_stat parser plus tests covering stat parsing and diff scenarios.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI User
    participant Cmd as diff::run()
    participant Client as Daemon Client
    participant Engine as Engine
    participant Git as git::diff_worktree()

    CLI->>Cmd: invoke diff (worktree, stat, json)
    activate Cmd
    Cmd->>Cmd: ensure daemon running\nbuild project_root
    Cmd->>Client: send Request::Diff(project_root, worktree_id, stat)
    deactivate Cmd

    activate Engine
    Client->>Engine: deliver Request::Diff
    Engine->>Engine: read manifest\nselect worktrees
    loop per worktree
        Engine->>Git: diff_worktree(path, base, stat)
        activate Git
        Git->>Git: run git diff / merge-base\ncompute --stat & parse
        Git-->>Engine: return DiffOutput (diff, files, ins, del)
        deactivate Git
    end
    Engine->>Engine: aggregate DiffResult
    Engine-->>Client: send Response::DiffResult
    deactivate Engine

    activate Cmd
    Client-->>Cmd: receive Response::DiffResult
    Cmd->>Cmd: validate response\nprint formatted output
    Cmd-->>CLI: display results
    deactivate Cmd
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through branches, sniffed each tree,
Collected diffs and stats for thee,
From CLI call to git’s soft hum,
Worktrees tallied, outputs done—
A crunchy carrot of changes, yum! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding a new pu diff command that displays changes across agent worktrees.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pu/diff-command

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/pu-core/src/protocol.rs`:
- Around line 238-244: The tagged IPC wire-format for Request::Diff and
Response::DiffResult changed but the protocol version was not bumped; update the
shared protocol version constant (e.g., PROTOCOL_VERSION or equivalent in
protocol.rs) to a new integer, add a runtime check where messages are
deserialized (before dispatching diff handling) to compare the incoming
message's protocol version against the compiled/expected version, and if they
mismatch immediately return an explicit error/fail-fast path that prevents
dispatching into Diff-related handlers; ensure the version bump is applied
consistently for both request and response handling code paths so older
daemons/clients detect incompatibility early.
- Around line 535-546: WorktreeDiffEntry currently lacks a structured
status/error field so engine.rs stuffs git failures into diff_output; add a new
field (e.g., pub status: WorktreeDiffStatus or pub error: Option<String>) to
WorktreeDiffEntry and define a small enum/option (e.g., WorktreeDiffStatus { Ok,
Error } with an optional error message or Option<String> error_message) that is
serde-serializable (keep rename_all = "snake_case"), then update
crates/pu-engine/src/engine.rs to set this new field on failure instead of
serializing errors into diff_output and reserve diff_output for actual diff text
(set diff_output to "" or None when error), and ensure
files_changed/insertions/deletions are set appropriately for error cases so JSON
clients can distinguish real empty diffs from failures.

In `@crates/pu-engine/src/engine.rs`:
- Around line 2975-2978: The code currently treats a missing worktree directory
(wt_path from wt.path) as “no diffs” by using `continue`, which hides deleted
worktrees; instead, when processing a specific worktree (e.g., the branch
handling `pu diff --worktree <id>`), return an error for that targeted worktree
(propagate Err or add an explicit error entry into the response) so callers can
distinguish a deleted worktree from a clean one; keep best-effort skipping only
for bulk/partial-result paths by detecting that mode and retaining the original
`continue` behavior there.

In `@crates/pu-engine/src/git.rs`:
- Around line 60-82: The diff_worktree function currently compares the worktree
against the tip of base (via run_git_allow_empty using "diff <base>|HEAD"),
which can include upstream commits as base advances and also omits untracked
files; update diff_worktree to (1) compute an explicit merge base using git
merge-base HEAD <base> and use that SHA as the comparison target when a base is
provided (call git via run_git_allow_empty to get the merge-base SHA), (2) if
callers expect uncommitted/untracked files to be included, add the flags
--others --exclude-standard (or document the exclusion) when invoking git diff,
and (3) update the function docstring to state the chosen semantic (merge-base
vs tip-of-base) and whether untracked files are included; keep parse_diff_stat
usage and stat calculation unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a8f27e9-0f8f-4b7f-9e1d-83b82576d3fb

📥 Commits

Reviewing files that changed from the base of the PR and between 044e2e5 and 24a71bb.

📒 Files selected for processing (7)
  • crates/pu-cli/src/commands/diff.rs
  • crates/pu-cli/src/commands/mod.rs
  • crates/pu-cli/src/main.rs
  • crates/pu-cli/src/output.rs
  • crates/pu-core/src/protocol.rs
  • crates/pu-engine/src/engine.rs
  • crates/pu-engine/src/git.rs

- Bump PROTOCOL_VERSION to 2 for the new Diff wire-format variants
- Add `error: Option<String>` to WorktreeDiffEntry so JSON clients can
  distinguish git failures from legitimate empty diffs
- Use `git merge-base HEAD <base>` instead of diffing against the tip of
  the base branch, so diffs show only the worktree's own changes
- Return an error entry (not silent skip) when `--worktree <id>` targets
  a missing directory; keep best-effort skipping for bulk queries
- Display error entries in human-readable output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/pu-engine/src/git.rs (1)

57-90: ⚠️ Potential issue | 🟠 Major

Untracked files are still invisible to pu diff.

A brand-new file will not appear until it is staged, so pu diff can still report “No changes” even when an agent created files in the worktree. The new doc comment documents the limitation, but the feature contract here is to surface worktree changes, not just tracked ones. Consider folding git ls-files --others --exclude-standard into both the rendered diff and the summary counts.

To verify, create an untracked file in a throwaway repo. Expected result: git diff HEAD stays empty while git ls-files --others --exclude-standard reports the new path.

#!/bin/bash
set -euo pipefail

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

git -C "$tmp" init -q
git -C "$tmp" config user.name Test
git -C "$tmp" config user.email test@example.com

printf 'tracked\n' > "$tmp/readme.txt"
git -C "$tmp" add readme.txt
git -C "$tmp" commit -qm init

printf 'new file\n' > "$tmp/new_file.rs"

echo '--- git diff HEAD --stat ---'
git -C "$tmp" diff HEAD --stat

echo '--- git diff HEAD ---'
git -C "$tmp" diff HEAD

echo '--- untracked files ---'
git -C "$tmp" ls-files --others --exclude-standard
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/pu-engine/src/git.rs` around lines 57 - 90, diff_worktree currently
ignores untracked files; call run_git_allow_empty with ["ls-files", "--others",
"--exclude-standard"] to get untracked paths, update parse_diff_stat usage to
include those counts (increment files_changed and insertions/deletions as
appropriate or at least files_changed += untracked.len()), and then fold the
untracked information into both outputs: for stat mode, append a summary line
for untracked files to stat_output before returning; for full-diff mode, append
a simple rendered block (e.g. a header and newline-separated untracked paths) to
the diff string returned by the run_git_allow_empty(["diff", target], ...) call.
Use the existing functions run_git_allow_empty, parse_diff_stat and the
diff_worktree return flow to locate where to inject the ls-files call and how to
merge the results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/pu-engine/src/git.rs`:
- Around line 57-82: The bug is that worktrees are being created with the
literal "HEAD" as their stored base_branch which makes diff_worktree (and its
merge-base logic using run_git_allow_empty and target computed from
merge_base/as_deref()) compare the worktree against its own tip and hide
commits; fix by resolving and storing a stable commit SHA (e.g., run git
rev-parse HEAD at creation time) or an explicit upstream branch name when
creating the worktree in the engine (references: the base_branch assignment in
engine.rs and diff_worktree in git.rs) so merge-base(HEAD, base) will compute
correctly, then update creation code to call git rev-parse and persist that SHA
instead of the literal "HEAD", and add a regression test that creates a
worktree, makes commits on the worktree branch, and asserts diff_worktree shows
those commits.

---

Duplicate comments:
In `@crates/pu-engine/src/git.rs`:
- Around line 57-90: diff_worktree currently ignores untracked files; call
run_git_allow_empty with ["ls-files", "--others", "--exclude-standard"] to get
untracked paths, update parse_diff_stat usage to include those counts (increment
files_changed and insertions/deletions as appropriate or at least files_changed
+= untracked.len()), and then fold the untracked information into both outputs:
for stat mode, append a summary line for untracked files to stat_output before
returning; for full-diff mode, append a simple rendered block (e.g. a header and
newline-separated untracked paths) to the diff string returned by the
run_git_allow_empty(["diff", target], ...) call. Use the existing functions
run_git_allow_empty, parse_diff_stat and the diff_worktree return flow to locate
where to inject the ls-files call and how to merge the results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9455366-9595-45fd-b6d9-d9a7d7f2b532

📥 Commits

Reviewing files that changed from the base of the PR and between 24a71bb and 3940e29.

📒 Files selected for processing (4)
  • crates/pu-cli/src/output.rs
  • crates/pu-core/src/protocol.rs
  • crates/pu-engine/src/engine.rs
  • crates/pu-engine/src/git.rs

When no base is supplied, worktrees were storing the literal "HEAD" as
base_branch. After introducing merge-base semantics in diff_worktree,
this caused `git merge-base HEAD HEAD` to return the worktree's own
tip, hiding all committed changes.

Now resolves HEAD to the actual branch name (e.g. "main") at worktree
creation time via `git symbolic-ref --short HEAD`, falling back to a
SHA for detached HEAD. This ensures diff_worktree correctly computes
the fork point and shows only the worktree's own changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@2witstudios 2witstudios merged commit 7b45828 into main Mar 7, 2026
5 checks passed
@2witstudios 2witstudios deleted the pu/diff-command branch March 7, 2026 16:30
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.

1 participant