Skip to content

feat(diff): reverse commit changes by file, hunk, or line + copy lines#42

Merged
the0807 merged 6 commits into
the0807:mainfrom
sehlceris:feature/revert-commit-changes
Jun 25, 2026
Merged

feat(diff): reverse commit changes by file, hunk, or line + copy lines#42
the0807 merged 6 commits into
the0807:mainfrom
sehlceris:feature/revert-commit-changes

Conversation

@sehlceris

@sehlceris sehlceris commented Jun 24, 2026

Copy link
Copy Markdown

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:

  • Reverse a whole file / hunk / selected lines — drag-select lines in the gutter, then reverse only the changed (+/-) lines, a whole hunk, or the whole file. Applied to the working tree via git apply --reverse --recount (no new commit, no index changes).
  • Copy Lines — copy the raw text of gutter-selected lines to the clipboard (gutter-only, scoped to the selection's hunk).

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 existing git revert feature (which creates a commit). All symbols, the reverseCommitChanges message, and the toast string use "reverse" consistently.

Backend

  • buildReversePatch builds a minimal reverse-applicable patch for one hunk, optionally narrowed to selected lines; webview line indices map directly onto the hunk's DiffLine[].
  • GitService.reverseCommitChanges applies it to the working tree; routed via a new reverseCommitChanges message.
  • \ No newline at end of file markers are reconstructed from the final hunk state, so partial-line reverses on files without a trailing newline no longer corrupt the result.

UI

  • Gutter drag-selection, hunk-header buttons, and a right-click menu (Reverse Hunk / Reverse Selected Lines / Copy / Copy Lines), scoped to the selection's hunk.
  • Gated to committed, reversible files (not pure adds/deletes, not the working-tree view).
  • i18n labels added for en/ko/zh.

Testing (against current main)

  • Patch-builder unit tests + real-git integration tests (incl. no-trailing-newline edge cases).
  • Webview tests for gutter selection, context-menu target building, and copy-lines gating.
  • Full suite: 1556 passed | 11 skipped. npm run build ✓, svelte-check 0/0, extension tsc --noEmit clean.

Demo Video

git-graph-plus-reverse-changes.2.mp4

Screenshots

Screenshot From 2026-06-23 10-38-23 Screenshot From 2026-06-23 10-38-53 Screenshot From 2026-06-23 10-39-03 Screenshot From 2026-06-23 10-39-11 Screenshot From 2026-06-23 22-32-30

sehlceris added 3 commits June 23, 2026 10:03
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

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.95918% with 4 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/git/patch-builder.ts 97.48% 2 Missing and 2 partials ⚠️

📢 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
@sehlceris sehlceris marked this pull request as ready for review June 24, 2026 05:49
@the0807

the0807 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Thanks for the thorough work here - the patch-builder index alignment with parseDiff and the no-newline reconstruction are really well done, and the test coverage is excellent.

One thing worth a look before merge: in rawCommitFileDiff, the merge-commit parent selection uses if (raw.trim()) to pick the first non-empty parent, but showCommitDiff (which renders what the user actually sees) picks the first parent where parsed[0].hunks.length > 0. These criteria can diverge: for a merge commit where an earlier parent yields a hunkless-but-nonempty diff for the file (a mode-only or rename-only change) while a later parent carries the real content hunks, the UI shows the later parent's hunks but reverse builds the patch from the earlier parent's diff. That ends up either throwing Hunk N not found for a partial reverse, or reversing an unexpected mode/rename change for a whole-file reverse.

The trigger is narrow, so it's not a blocker, but since rawCommitFileDiff and showCommitDiff duplicate the root/merge/single-parent selection logic, the cleanest fix is probably to extract a shared helper that returns the raw diff together with the chosen parent, so the displayed diff and the reversed diff can never select different parents. Could you take a look?

sehlceris added 2 commits June 24, 2026 18:54
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
@sehlceris

Copy link
Copy Markdown
Author

Thanks for the thorough work here - the patch-builder index alignment with parseDiff and the no-newline reconstruction are really well done, and the test coverage is excellent.

One thing worth a look before merge: in rawCommitFileDiff, the merge-commit parent selection uses if (raw.trim()) to pick the first non-empty parent, but showCommitDiff (which renders what the user actually sees) picks the first parent where parsed[0].hunks.length > 0. These criteria can diverge: for a merge commit where an earlier parent yields a hunkless-but-nonempty diff for the file (a mode-only or rename-only change) while a later parent carries the real content hunks, the UI shows the later parent's hunks but reverse builds the patch from the earlier parent's diff. That ends up either throwing Hunk N not found for a partial reverse, or reversing an unexpected mode/rename change for a whole-file reverse.

The trigger is narrow, so it's not a blocker, but since rawCommitFileDiff and showCommitDiff duplicate the root/merge/single-parent selection logic, the cleanest fix is probably to extract a shared helper that returns the raw diff together with the chosen parent, so the displayed diff and the reversed diff can never select different parents. Could you take a look?

Thank you for the catch! The suggested refactor has been completed. Please let me know if you see any other potential improvements!

@the0807 the0807 merged commit a386b93 into the0807:main Jun 25, 2026
3 checks passed
@the0807

the0807 commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Merged - thank you for this careful and excellent contribution! This will be included in the next release.

@the0807

the0807 commented Jun 25, 2026

Copy link
Copy Markdown
Owner

v0.6.0 has been released.

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