Skip to content

feat(staging): add hunk- and line-level staging to the diff viewer#167

Open
liam-russell wants to merge 3 commits into
mainfrom
feat/hunk-staging
Open

feat(staging): add hunk- and line-level staging to the diff viewer#167
liam-russell wants to merge 3 commits into
mainfrom
feat/hunk-staging

Conversation

@liam-russell

Copy link
Copy Markdown
Contributor

Type

  • feat — new feature or user-visible capability

Summary

  • Added per-hunk "Stage hunk" / "Unstage hunk" buttons (revealed on hover) to the diff viewer in the Changes panel.
  • Added per-line checkboxes on added/removed lines within a hunk, so a subset of a hunk's changes can be staged or unstaged independently.
  • Fixed a bug where a file's status could be misreported (wrong path, wrong staged/unstaged columns) when it was the only entry in git status and its change was unstaged-only.

Why

Staging was previously file-level only (#89). Partial staging is table stakes for a Git GUI, and it pairs naturally with reviewing AI-generated diffs — "keep this hunk, discard that one."

Screenshots / recordings

Verified interactively via a WebdriverIO E2E run against the real Electron app (screenshots captured locally, not embeddable through this CLI session):

  • Diff view for a modified notes.txt renders two separate @@ ... @@ hunks, each as its own group.
  • Hovering a hunk reveals a "Stage hunk" button in its header.
  • Added/removed lines each have a checkbox (checked by default); unchecking some flips the button label to "Stage selected"/"Unstage selected" and scopes the patch to just those lines.
  • After staging one hunk, the file appears in both the Unstaged and Staged lists, each showing only its own remaining/staged hunk.

Breaking changes

None. Two existing IPC calls used to feed the diff viewer's "unstaged" and "staged" panes (getWorkingDiff / getDiffContent(..., 'HEAD', ...)) were replaced with two new dedicated ones (getUnstagedFileDiff, getStagedFileDiff) that diff against the index instead of HEAD — needed so the hunk indices shown in the UI line up with what stageHunk/unstageHunk recompute server-side. getWorkingDiff/GIT_WORKING_DIFF are left in place, just no longer used by this panel.

Test plan

  • Unit tests for the pure hunk-parsing/patch-construction logic (packages/types/src/__tests__/diff-hunks.test.ts), covering multi-hunk parsing, no-trailing-newline handling, and line-level selection (dropping a deselected add, keeping a deselected del as context, and recounting the hunk header).
  • Integration tests against real git repos for stageHunk/unstageHunk (packages/git/src/__tests__/staging-hunks.test.ts): staging/unstaging a single hunk out of two, staging a subset of lines within a hunk, and a no-trailing-newline round trip.
  • Regression unit test for the getWorktreeStatus parsing bug (packages/git/src/__tests__/git.test.ts).
  • New E2E case in e2e/specs/commit-workflow.spec.ts: stages one hunk of a two-hunk file, verifies the file shows up in both Staged and Unstaged with the correct hunk in each, then unstages it back.
  • pnpm lint && pnpm typecheck && pnpm test (including the full E2E suite) all pass.

Adds stage/unstage controls per diff hunk, with optional per-line
selection for partial-hunk staging, via patches built from the current
diff and applied with `git apply --cached` (`--reverse` to unstage).

Also fixes a latent bug in getWorktreeStatus: simple-git's `trimmed: true`
option strips the leading space of a porcelain status line like " M file"
when it's the first line of the whole output, corrupting the parsed path
and status columns for a file with only an unstaged modification.

Closes #89

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 11:23

Copilot AI 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.

Pull request overview

This PR adds partial staging (hunk- and line-level) to the Changes diff viewer by introducing shared diff hunk parsing/patch construction logic and new git IPC endpoints to stage/unstage specific hunks or selected lines.

Changes:

  • Add per-hunk stage/unstage controls and per-line selection checkboxes to the diff viewer UI.
  • Introduce shared unified-diff parsing + single-hunk patch construction utilities in @sproutgit/types (with tests).
  • Add git-layer support + IPC wiring for staged/unstaged file diffs and hunk/line staging, plus a status parsing regression fix.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ui/src/styles.css Updates diff hunk header layout and adds checkbox cursor styling.
packages/ui/src/components/StagingPanel.tsx Renders per-hunk diff UI with stage/unstage actions and per-line selection state.
packages/types/src/ipc.ts Adds IPC channels/types for hunk staging and staged/unstaged file diffs.
packages/types/src/index.ts Re-exports the new diff-hunk utilities module.
packages/types/src/diff-hunks.ts Implements parseFileDiff and buildHunkPatch shared by UI and git logic.
packages/types/src/tests/diff-hunks.test.ts Unit tests for diff parsing and line-level patch construction.
packages/git/src/staging.ts Adds stageHunk / unstageHunk and fixes porcelain status parsing edge case.
packages/git/src/index.ts Exposes new staging helpers and getUnstagedFileDiff from the git package.
packages/git/src/diff.ts Extends staged diff to optionally scope to a file and adds worktree-vs-index per-file diff.
packages/git/src/tests/staging-hunks.test.ts Integration tests for staging/unstaging hunks and selected lines.
packages/git/src/tests/git.test.ts Regression test for the getWorktreeStatus porcelain parsing bug.
e2e/specs/commit-workflow.spec.ts E2E coverage for staging/unstaging a single hunk and verifying staged/unstaged panes.
app/src/renderer/routes/workspace.tsx Wires new diff endpoints and hunk staging methods into the staging panel.
app/src/preload/index.ts Adds preload API methods to invoke new git IPC endpoints.
app/src/main/ipc/git.ts Registers new main-process handlers for hunk staging and staged/unstaged file diffs.

Comment on lines +262 to +265
const parsed = parseFileDiff(raw);
if (!parsed || parsed.hunks.length === 0) {
return <span className="sg-diff-empty">No changes</span>;
}
data-hunk-index={hunkIndex}
className="sg-stage-hunk-btn inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-(--sg-surface) border border-(--sg-border) text-(--sg-text-dim) hover:bg-(--sg-surface-raised) hover:text-(--sg-text) disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
onClick={runStage}
disabled={stagePending || nothingSelected}
data-hunk-index={hunkIndex}
className="sg-unstage-hunk-btn inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-(--sg-surface) border border-(--sg-border) text-(--sg-text-dim) hover:bg-(--sg-surface-raised) hover:text-(--sg-text) disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
onClick={runUnstage}
disabled={unstagePending || nothingSelected}
className="sg-diff-line-checkbox mt-[3px] shrink-0"
data-testid="diff-line-checkbox"
data-hunk-index={hunkIndex}
data-line-index={idx}
# Conflicts:
#	app/src/renderer/routes/workspace.tsx
#	packages/git/src/staging.ts
#	packages/types/src/index.ts
# Conflicts:
#	app/src/main/ipc/git.ts
#	app/src/preload/index.ts
#	packages/types/src/ipc.ts
#	packages/ui/src/components/StagingPanel.tsx
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.

2 participants