feat(staging): interactive hunk staging#8
Merged
poolcamacho merged 3 commits intomasterfrom Apr 20, 2026
Merged
Conversation
… builder, stdin support First of 4 stages for Interactive Staging (#roadmap). No user-visible change yet; this commit adds the plumbing that Stage 2 (UI) and Stage 3 (coordinator) will build on top of. Models - DiffHunk: groups the lines under a single '@@ ... @@' header, keeping the raw header text so we can reconstruct patches without reformatting. - DiffFile: preamble (diff --git / index / --- / +++) + hunks. Previously the parser discarded the preamble; it has to stay in the tree so git apply accepts the patch. - DiffFile.patchText(forHunkIndices:) builds a git-apply-compatible patch including only the selected hunks. Valid for whole-hunk selection; the later line-level stage will rewrite the header counts for partials. Parser - New DiffParser enum with parseFiles(_:) and parseFlat(_:). - Internal State struct handles the per-line transitions cleanly instead of the local-var soup the previous parser used. - Two thin aliases remain on GitService (parseDiffFiles, parseDiff) so existing callers keep working unchanged. GitService - run(...) accepts an optional stdin: String. When non-nil, a Pipe is used for stdin and the string is written + closed after process.run() so git sees EOF. Falls back to the existing /dev/null behaviour when stdin is nil, so every other caller is unaffected. - applyPatch(_:cached:reverse:in:) wraps 'git apply --whitespace=nowarn -' with the provided patch piped in. cached=true stages, reverse=true unstages. Build clean, swiftlint --strict clean.
Stage 2 + Stage 3 of the interactive-staging plan. Whole-hunk only; per-line selection follows in a later PR. - AppState: currentDiffFile + selectedHunks alongside the existing flat DiffLine stream. Selection resets on file change / repo reload. - GitService.diffFile(for:staged:in:) returns the structured DiffFile so the coordinator can build partial patches without a second subprocess roundtrip. - GitCoordinator: stageSelectedHunks / unstageSelectedHunks build the patch via DiffFile.patchText and pipe it to git apply --cached (with --reverse when unstaging). - DiffView: optional diffFile + selection binding. When both are present, renders from hunks with a checkbox on each @@ header row. Commit-history diffs keep the flat, read-only path. - ChangesTabView: HunkStagingToolbar above the diff — Select all, counter, and Stage|Unstage selected (flips based on isStaged). Only rendered for tracked files (untracked and conflicted skip).
Adds a Features bullet for interactive staging and moves hunk-level support to Done. Line-level selection stays under Next.
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.
Summary
Interactive staging at hunk granularity. Tick a checkbox on any
@@header to stage (or unstage) only those hunks — no moregit add -pin the terminal. Per-line selection ships in a follow-up PR.What changed
Stage 1 — foundation (
c29d5f7)DiffHunk/DiffFilemodels grouping lines under their@@header and preserving the file preamble (diff --git,index,---,+++) needed to reconstruct a patch.DiffFile.patchText(forHunkIndices:)— rebuilds agit apply-compatible patch from any subset of hunks.Services/DiffParser.swiftextracted fromGitService—parseFiles(structured) andparseFlat(legacy).GitService.run(...)now takes an optionalstdin: String?so we can pipe patches directly.GitService.applyPatch(_:cached:reverse:in:)wrapsgit apply --whitespace=nowarn -with optional--cachedand--reverse.Stage 2 + 3 — UI + wiring (
e2a960a)AppStategainscurrentDiffFile: DiffFile?andselectedHunks: Set<Int>alongside the existing flatDiffLinestream. Selection resets on file change and repo reload.GitService.diffFile(for:staged:in:)returns the structured parse so the coordinator skips a second subprocess.GitCoordinator.stageSelectedHunks()/unstageSelectedHunks()build the patch viaDiffFile.patchTextand pipe it togit apply --cached(with--reverseto unstage).DiffViewtakes optionaldiffFile+selectionbinding. When present, renders from hunks with a checkbox on each@@row. Commit-history diffs keep the flat read-only path.ChangesTabViewaddsHunkStagingToolbarabove the diff — Select all, counter, Stage / Unstage selected (flips based onisStaged). Only rendered for tracked files; untracked and conflicted skip.Docs (
2f3712a)Not in this PR
oldCount/newCountand handle context lines carefully. Separate PR.Test plan
@@header → click Stage selected → only that hunk lands in the index (git diff --cachedconfirms)swiftlint --strictclean,xcodebuildclean