feat(staging): add hunk- and line-level staging to the diff viewer#167
Open
liam-russell wants to merge 3 commits into
Open
feat(staging): add hunk- and line-level staging to the diff viewer#167liam-russell wants to merge 3 commits into
liam-russell wants to merge 3 commits into
Conversation
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>
Contributor
There was a problem hiding this comment.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type
Summary
git statusand 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):
notes.txtrenders two separate@@ ... @@hunks, each as its own group.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 ofHEAD— needed so the hunk indices shown in the UI line up with whatstageHunk/unstageHunkrecompute server-side.getWorkingDiff/GIT_WORKING_DIFFare left in place, just no longer used by this panel.Test plan
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).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.getWorktreeStatusparsing bug (packages/git/src/__tests__/git.test.ts).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.