Skip to content

feat: match a hunk against its approval-time counterpart - #198

Draft
asyncawaitpromise wants to merge 1 commit into
feat/ignore-formattingfrom
feat/normalized-hunk-dedup
Draft

feat: match a hunk against its approval-time counterpart#198
asyncawaitpromise wants to merge 1 commit into
feat/ignore-formattingfrom
feat/normalized-hunk-dedup

Conversation

@asyncawaitpromise

Copy link
Copy Markdown
Collaborator

Related PR(s)

Depends on #186. It sits beside #187, #188 and #189 rather than on top of them, because it changes the subtraction rather than any one flag, so it composes with whichever of those land.

Summary / Background

changesSince takes two three-dot diffs from the same merge base and subtracts one from the other by hunk hash. Both are anchored at the base, so an edit that shifts a hunk's boundaries re-anchors that hunk over lines the reviewer already approved, and it survives because its bytes no longer match.

isTrivial cannot recover that case. Say a reviewer approves a new 30 line function and the author then adds one comment inside it. The surviving hunk is the whole function, and asked "is this trivial?", the honest answer is no, it is thirty lines of new code. So we dismiss over one comment.

The question worth asking is not is this hunk trivial but is this hunk trivially different from what was approved, and only comparing the two sides can answer that.

The practical effect today is that a flag only fires when the trivial edit lands on a line the PR had not already touched. Tidying up code you added earlier in the same branch still dismisses, which is the common case.

Code Changes

Build a second lookup beside the raw hashes, keyed on each approval-time hunk's body after the enabled flags' noise is stripped, and drop a current hunk that matches one.

The raw hash still decides first, so this can only ever remove hunks that were previously kept. It never keeps a hunk that was previously removed.

Two places where the key is deliberately narrower than the raw hash:

  • The file name is part of it. A normalized body says less than a raw one, so identity should not reach as far; the same text arriving in a second file is new code there. The raw hash does reach across files, which is fine because it compares bytes.
  • No enabled steps means no key at all. hunkHash reads the added and removed lines interleaved, so a key built from the two sides separately is looser even with zero normalization. Gating on the step list keeps de-duplication exactly as it is for anyone who has not turned a flag on. There is a test asserting that.

Tests cover the boundary-shift case with the flag on and off, a changed call target still surfacing however aggressively it is normalized, the same line in a different file, and the no-key-when-off rule.

One thing this makes more reachable

Flattening nested SCSS selectors normalizes equal, because braces are dropped and whitespace collapses. That happens to be a correct CSS equivalence for a plain de-nest, but there is no CSS model behind it, so one changing specificity or involving & would normalize equal just as readily.

That is not introduced here, and it is rare enough today that it does not come up. It is a lot more reachable with this than without it, and I would rather name it than have it found later.

changesSince subtracts the approval-time diff from the current one on the raw
hunk body. Both diffs are taken from the merge base, so an edit that shifts a
hunk's boundaries re-anchors it over lines the reviewer already approved: the
hunk then differs byte for byte from its counterpart and survives.

isTrivial cannot recover that case. It asks whether a hunk is trivial, and a
hunk spanning a whole new function is not, however little of it changed since
the approval. The question worth asking is whether the hunk is trivially
*different from what was approved*, which needs both sides.

So build a second lookup keyed on the normalized body of each approval-time
hunk, and drop a current hunk that matches one. The raw hash still decides
first, so this only ever removes hunks that were previously kept.

Deliberately narrower than the raw hash in two ways. The file name is part of
the key, because a normalized body says less than a raw one and identity should
not extend across files. And no enabled steps means no key at all: the raw hash
reads the two sides interleaved, so a key built from them separately is looser,
and de-duplication must not change for anyone who has not enabled a flag.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an approval key mechanism to track and match shifted or normalized hunks against previous approvals, preventing unnecessary re-reviews. It includes changes to hunk filtering logic in internal/git/diff.go, the addition of the approvalKey method in internal/git/normalize.go, and comprehensive unit tests. The review feedback suggests adding defensive nil checks for diff files and hunks across these files to prevent potential nil pointer dereference panics.

Comment thread internal/git/diff.go
oldHunkHashes := make(map[[32]byte]bool)
oldApprovalKeys := make(map[string]bool)
for _, d := range context.olderDiff {
fileName := diffToFilename(d)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To prevent a potential nil pointer dereference panic, we should defensively check if d is nil before calling diffToFilename(d).

		if d == nil {
			continue
		}
		fileName := diffToFilename(d)

Comment thread internal/git/diff.go
Hunks: make([]codeowners.HunkRange, 0, len(d.Hunks)),
}
for _, hunk := range d.Hunks {
if oldHunkHashes[hunkHash(hunk)] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To prevent a potential nil pointer dereference panic, we should defensively check if hunk is nil before passing it to hunkHash.

			if hunk == nil {
				continue
			}
			if oldHunkHashes[hunkHash(hunk)] {

Comment thread internal/git/normalize.go
if len(n.steps) == 0 {
return "", false
}
added, removed, ok := hunkBlocks(hunk.Body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To prevent a potential nil pointer dereference panic, we should defensively check if hunk is nil before accessing its fields or passing it to hunkBlocks.

Suggested change
added, removed, ok := hunkBlocks(hunk.Body)
if hunk == nil {
return "", false
}
added, removed, ok := hunkBlocks(hunk.Body)

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.

1 participant