Skip to content

feat: retain approvals across formatting-only hunks - #186

Draft
asyncawaitpromise wants to merge 1 commit into
feat/approval-retention-configfrom
feat/ignore-formatting
Draft

feat: retain approvals across formatting-only hunks#186
asyncawaitpromise wants to merge 1 commit into
feat/approval-retention-configfrom
feat/ignore-formatting

Conversation

@asyncawaitpromise

@asyncawaitpromise asyncawaitpromise commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Related PR(s)

Depends on #183. Carries the shared normalizer the other content flags are steps on, so it lands first among them. These build on it:

Summary / Background

The first content flag, plus the engine the rest plug into.

The design decision that matters: compare blocks, not lines

Everything a hunk adds is joined into one string and everything it removes into another, then both are normalized and compared whole. A line-wise comparison can never pair up a statement rewrapped across several lines, because one removed line becomes three added ones:

-        if (!isOpen) return
+        if (!isOpen) {
+            return
+        }

Block-wise, both sides normalize to the same text.

What formatting ignores

Whitespace, braces, semicolons and trailing commas.

Parentheses are deliberately kept. Their placement separates a call from a reference (handler vs handler()) and decides precedence ((a+b)*c vs a+(b*c)), so dropping them would hide real changes behind a formatting flag.

An empty pair of braces is kept for the same reason: an empty argument, body or literal says something the code does not say without it.

Where it hooks in

changesSince in internal/git/diff.go. The hunk loop already filters on !oldHunkHashes[hunkHash(hunk)]; the triviality check is a second condition on the same loop, reading the same bytes hunkHash already reads. No new git calls.

File layout

One file per flag — the engine and its lexical helpers here, each later flag in its own file. The flags can then be reviewed and enabled independently, and the only shared edit point is the ordered if-chain in newNormalizer.

That chain is deliberately explicit rather than a registry: step order is load-bearing (comments must come off before the punctuation steps), and an explicit chain states the constraint where a priority number would hide it.

@gemini-code-assist

Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown

Confidence Score: 2/5

This PR should not merge until formatting retention stops masking control-flow scope changes and punctuation changes inside string values.

The new normalizer can classify substantive hunks as trivial, causing ChangesSince to omit them and preserve approvals that should be dismissed; one path can bypass renewed review of authorization-sensitive scope changes.

Files Needing Attention: internal/git/normalize_formatting.go, internal/git/normalize.go, internal/git/diff.go

Security Review

Formatting normalization erases block boundaries, allowing a token-preserving authorization or control-flow scope change to retain an existing approval. How this was verified: Tracing a brace-scope edit through whole-hunk normalization shows identical normalized token sequences are omitted from the stale-approval diff.

Important Files Changed

Filename Overview
internal/git/normalize_formatting.go Introduces punctuation normalization that can erase control-flow scope and semantic punctuation inside string literals.
internal/git/normalize.go Adds whole-hunk block normalization and triviality classification; its lexical and structural fidelity depends on each normalization step.
internal/git/diff.go Wires normalization into ChangesSince so misclassified hunks directly affect stale-approval detection.
internal/app/app.go Correctly passes protected base-branch approval-retention configuration into diff construction.
internal/git/normalize_formatting_test.go Covers intended formatting transformations but omits scope-changing brace edits and punctuation-only string changes.
README.md Documents block comparison and the intentionally broad formatting behavior, including scope-sensitive transformations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Base-branch approval retention config] --> B[GitDiff.ChangesSince]
  B --> C[Split hunk into added and removed blocks]
  C --> D[collapseFormatting]
  D --> E{Normalized blocks equal?}
  E -->|Yes| F[Omit hunk from approval diff]
  E -->|No| G[Report hunk as changed]
  F --> H[Existing approval retained]
  G --> I[Approval may be dismissed]
Loading

Reviews (1): Last reviewed commit: "feat: retain approvals across formatting..." | Re-trigger Greptile

Comment on lines +46 to +49
if unicode.IsSpace(r) || strings.ContainsRune(structuralPunctuation, r) {
pendingSpace = b.Len() > 0
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Block scope changes retain approvals

When formatting retention is enabled, removing non-empty braces lets a contiguous edit move a statement into or out of a conditional or authorization block while producing the same normalized token sequence. ChangesSince then omits the hunk, causing an existing approval to remain valid after a control-flow or authorization-scope change.

How this was verified: Tracing a brace-scope edit through whole-hunk normalization shows that discarded braces leave identical token sequences, which are omitted from the stale-approval diff.

Comment on lines +44 to +52
r, size := utf8.DecodeRuneInString(block[i:])
i += size
if unicode.IsSpace(r) || strings.ContainsRune(structuralPunctuation, r) {
pendingSpace = b.Len() > 0
continue
}
writeToken(string(r), r, r)
}
return dropTrailingCommas(b.String())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 String punctuation bypasses stale detection

When formatting retention is enabled, collapseFormatting removes braces, semicolons, whitespace, and trailing-comma patterns without recognizing string boundaries. A punctuation-only change to a URL, query, template, command, or other string value can therefore normalize away, causing the existing approval to remain valid even though string_literals is disabled.

@asyncawaitpromise
asyncawaitpromise marked this pull request as draft August 21, 2026 21:33
Carries the shared normalizer the other content flags are steps on, plus the
first of those flags.

The design decision that matters is block-wise comparison. Everything a hunk
adds is joined into one string and everything it removes into another, and the
two are normalized and compared whole. Line-wise comparison cannot pair up a
statement rewrapped across several lines, because one removed line becomes
three added ones:

    -        if (!isOpen) return
    +        if (!isOpen) {
    +            return
    +        }

Block-wise, both sides normalize to the same text.

formatting collapses whitespace, braces, semicolons and trailing commas.
Parentheses are deliberately kept: their placement separates a call from a
reference and decides precedence, so dropping them would hide real changes.
A pair of braces closing over nothing is kept for the same reason, since an
empty argument or body says something the code does not say without it.

The engine hooks into the existing hunk loop in changesSince, reading the same
bytes hunkHash already reads, so there are no new git calls.

One file per flag: the engine and its lexical helpers here, each later flag in
its own file, so the flags can be reviewed and enabled independently.
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