feat(diff): reverse commit changes by file, hunk, or line + copy lines#42
Conversation
Add backend support for undoing part of a past commit against the working tree. `buildReversePatch` constructs a minimal, reverse-applicable patch for a single hunk (optionally narrowed to selected lines), and `GitService.reverseCommitChanges` applies it with `git apply --reverse --recount`. A new `reverseCommitChanges` message routes the request through MainPanel, which reports completion and shows the reverse toast. Named "reverse" (not "revert") because it reverse-applies to the working tree without creating a commit, and to distinguish it from the existing `git revert`. The patch builder reconstructs `\ No newline at end of file` markers from the final hunk state rather than re-emitting them per source line, so partial-line reverses on files without a trailing newline no longer corrupt the result. Covered by patch-builder unit tests and real-git integration tests, including no-trailing-newline edge cases. Claude-Session: https://claude.ai/code/session_01B3yKdHuNTMUk6foSE96buB
Add reverse affordances to the commit-details inline diff: reverse a whole hunk, reverse a gutter-selected set of changed lines, and copy the raw text of selected lines to the clipboard. Lines are selected by dragging in the gutter; hunk-header buttons and the right-click menu expose the actions, scoped to the hunk holding the selection. Copy Lines is gutter-only and treats an empty selection (a single blank line) as valid. Actions are gated to committed, reversible files (not pure adds/deletes, not the working-tree view). Adds i18n strings for the new labels in en/ko/zh. Claude-Session: https://claude.ai/code/session_01B3yKdHuNTMUk6foSE96buB
Whole-file additions and deletions now support the same reverse hunk / reverse selected lines affordances as modified files, instead of only the tree's "Reverse File" action. The webview no longer gates the reverse menu/buttons on file status. On the backend, the patch builder rewrites the whole-file header for partial selections: a `new file mode` / `deleted file mode` + `/dev/null` header only reverse-applies while one side stays empty, so a partial reverse (which leaves content on the formerly-empty side) is converted into an in-place modification header. Whole-hunk reverses keep the original header and still reverse-apply to a file delete/create. Claude-Session: https://claude.ai/code/session_016WeNfpnVAw4acXjBawtbWG
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Add tests for the previously-uncovered paths in the reverse-changes feature: - patch-builder: no-hunk diffs, space-stripped blank context lines, unparseable hunk headers, the whole-file-delete header rewrite, and a no-EOF deleted-file partial reverse. - git-service: reverseCommitChanges against a root commit (empty-tree diff) and a merge commit (first non-empty parent diff), plus the no-changes-to-reverse case on a merge. Raises patch coverage on patch-builder.ts and git-service.ts; remaining uncovered branches are unreachable defensive fallbacks. Claude-Session: https://claude.ai/code/session_01F1XM63RwFX5AN5KrAcbmyN
|
Thanks for the thorough work here - the patch-builder index alignment with One thing worth a look before merge: in The trigger is narrow, so it's not a blocker, but since |
showCommitDiff selected a merge parent by parsed hunk presence while rawCommitFileDiff selected by raw non-emptiness. For a merge commit where an earlier parent yields a hunkless mode-only/rename-only diff and a later parent carries the content hunks, the two diverged: the UI showed the later parent while reverse built its patch from the earlier one, throwing "Hunk N not found" on a partial reverse or reversing an unexpected mode/rename change on a whole-file reverse. Extract a shared commitFileDiff helper returning both raw and parsed diff so the displayed diff and the reversed patch can never select different parents. Add an integration test covering the hunkless-first-parent case. Claude-Session: https://claude.ai/code/session_01DvR8D2L1Q8o9bTo8jh2J4P
Thank you for the catch! The suggested refactor has been completed. Please let me know if you see any other potential improvements! |
|
Merged - thank you for this careful and excellent contribution! This will be included in the next release. |
|
v0.6.0 has been released. |
Unsure if this feature fits with your project direction, but I implemented for myself, so offering it just in case you would like it.
Summary
Adds the ability to undo part of a past commit against the working tree, directly from the commit-details inline diff:
+/-) lines, a whole hunk, or the whole file. Applied to the working tree viagit apply --reverse --recount(no new commit, no index changes).Rebased onto current
main(0.5.4); two cohesive commits (backend support, then UI).Naming
The operation is called reverse, not revert: it reverse-applies a patch to the working tree without creating a commit, mirroring
git apply --reverse. This also distinguishes it from the existinggit revertfeature (which creates a commit). All symbols, thereverseCommitChangesmessage, and the toast string use "reverse" consistently.Backend
buildReversePatchbuilds a minimal reverse-applicable patch for one hunk, optionally narrowed to selected lines; webview line indices map directly onto the hunk'sDiffLine[].GitService.reverseCommitChangesapplies it to the working tree; routed via a newreverseCommitChangesmessage.\ No newline at end of filemarkers are reconstructed from the final hunk state, so partial-line reverses on files without a trailing newline no longer corrupt the result.UI
Testing (against current main)
npm run build✓, svelte-check 0/0, extensiontsc --noEmitclean.Demo Video
git-graph-plus-reverse-changes.2.mp4
Screenshots