-
Notifications
You must be signed in to change notification settings - Fork 12
feat: match a hunk against its approval-time counterpart #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
asyncawaitpromise
wants to merge
1
commit into
feat/ignore-formatting
Choose a base branch
from
feat/normalized-hunk-dedup
base: feat/ignore-formatting
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+192
−6
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,9 +180,14 @@ func changesSince(context changesSinceContext) ([]codeowners.DiffFile, error) { | |
| // For each file, filter out hunks that are in oldDiff | ||
| // if len(hunks) > 0, add to diffFiles | ||
| oldHunkHashes := make(map[[32]byte]bool) | ||
| oldApprovalKeys := make(map[string]bool) | ||
| for _, d := range context.olderDiff { | ||
| fileName := diffToFilename(d) | ||
| for _, h := range d.Hunks { | ||
| oldHunkHashes[hunkHash(h)] = true | ||
| if key, ok := context.normalizer.approvalKey(fileName, h); ok { | ||
| oldApprovalKeys[key] = true | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -196,15 +201,23 @@ func changesSince(context changesSinceContext) ([]codeowners.DiffFile, error) { | |
| Hunks: make([]codeowners.HunkRange, 0, len(d.Hunks)), | ||
| } | ||
| for _, hunk := range d.Hunks { | ||
| if oldHunkHashes[hunkHash(hunk)] { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| continue | ||
| } | ||
| // The raw hash decided first, so this only ever removes a hunk that | ||
| // would otherwise have been kept. | ||
| if key, ok := context.normalizer.approvalKey(fileName, hunk); ok && oldApprovalKeys[key] { | ||
| continue | ||
| } | ||
| // A hunk which normalizes away leaves nothing new to review. The file | ||
| // name goes too: what normalizes away depends on the language. | ||
| if !oldHunkHashes[hunkHash(hunk)] && !context.normalizer.isTrivial(fileName, hunk) { | ||
| newHunkRange := codeowners.HunkRange{ | ||
| Start: int(hunk.NewStartLine), | ||
| End: int(hunk.NewStartLine + hunk.NewLines - 1), | ||
| } | ||
| newDiffFile.Hunks = append(newDiffFile.Hunks, newHunkRange) | ||
| if context.normalizer.isTrivial(fileName, hunk) { | ||
| continue | ||
| } | ||
| newDiffFile.Hunks = append(newDiffFile.Hunks, codeowners.HunkRange{ | ||
| Start: int(hunk.NewStartLine), | ||
| End: int(hunk.NewStartLine + hunk.NewLines - 1), | ||
| }) | ||
| } | ||
| // Binary files have no hunks; staleness is intentionally not tracked | ||
| // for them (there is no hunk content to hash against the older diff). | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| package git | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| owners "github.com/multimediallc/codeowners-plus/internal/config" | ||
| "github.com/sourcegraph/go-diff/diff" | ||
| ) | ||
|
|
||
| func parseDiffOrFail(t *testing.T, text string) []*diff.FileDiff { | ||
| t.Helper() | ||
| parsed, err := diff.ParseMultiFileDiff([]byte(text)) | ||
| if err != nil { | ||
| t.Fatalf("parsing diff: %v", err) | ||
| } | ||
| return parsed | ||
| } | ||
|
|
||
| // Every flag is set explicitly so a default-on one cannot ride along unasked. | ||
| func retention(formatting bool) *owners.ApprovalRetention { | ||
| no, yes := false, true | ||
| f := &no | ||
| if formatting { | ||
| f = &yes | ||
| } | ||
| return &owners.ApprovalRetention{ | ||
| Enabled: true, Whitespace: &no, Comments: &no, | ||
| Formatting: f, StringLiterals: &no, Renames: &no, | ||
| } | ||
| } | ||
|
|
||
| // An edit that shifts a hunk's boundaries re-anchors it over already-approved | ||
| // lines, so the hunk survives the raw hash and is too substantial to be trivial. | ||
| func TestChangesSinceMatchesShiftedHunkAgainstApproval(t *testing.T) { | ||
| // A whole new function, then the call rewrapped: the only change since approval. | ||
| const approved = `diff --git a/svc.go b/svc.go | ||
| --- a/svc.go | ||
| +++ b/svc.go | ||
| @@ -10,0 +11,3 @@ | ||
| +func Handle(r *Request) error { | ||
| + return dispatch(r, opts, deadline) | ||
| +}` | ||
| const current = `diff --git a/svc.go b/svc.go | ||
| --- a/svc.go | ||
| +++ b/svc.go | ||
| @@ -10,0 +11,7 @@ | ||
| +func Handle(r *Request) error { | ||
| + return dispatch( | ||
| + r, | ||
| + opts, | ||
| + deadline, | ||
| + ) | ||
| +}` | ||
|
|
||
| tt := []struct { | ||
| name string | ||
| retention *owners.ApprovalRetention | ||
| wantFiles int | ||
| }{ | ||
| {"formatting enabled: nothing new to review", retention(true), 0}, | ||
| {"formatting disabled: the hunk stands", retention(false), 1}, | ||
| {"retention off entirely: the hunk stands", nil, 1}, | ||
| } | ||
|
|
||
| for _, tc := range tt { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| got, err := changesSince(changesSinceContext{ | ||
| newerDiff: parseDiffOrFail(t, current), | ||
| olderDiff: parseDiffOrFail(t, approved), | ||
| normalizer: newNormalizer(tc.retention), | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("changesSince: %v", err) | ||
| } | ||
| if len(got) != tc.wantFiles { | ||
| t.Errorf("got %d changed files, want %d", len(got), tc.wantFiles) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // A hunk carrying real change survives however aggressively it is normalized. | ||
| func TestChangesSinceKeepsRealChangeAgainstApproval(t *testing.T) { | ||
| const approved = `diff --git a/svc.go b/svc.go | ||
| --- a/svc.go | ||
| +++ b/svc.go | ||
| @@ -10,0 +11,3 @@ | ||
| +func Handle(r *Request) error { | ||
| + return dispatch(r) | ||
| +}` | ||
| const current = `diff --git a/svc.go b/svc.go | ||
| --- a/svc.go | ||
| +++ b/svc.go | ||
| @@ -10,0 +11,3 @@ | ||
| +func Handle(r *Request) error { | ||
| + return dispatchAsync(r) | ||
| +}` | ||
|
|
||
| got, err := changesSince(changesSinceContext{ | ||
| newerDiff: parseDiffOrFail(t, current), | ||
| olderDiff: parseDiffOrFail(t, approved), | ||
| normalizer: newNormalizer(retention(true)), | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("changesSince: %v", err) | ||
| } | ||
| if len(got) != 1 { | ||
| t.Fatalf("a changed call target must still be reviewable, got %d files", len(got)) | ||
| } | ||
| } | ||
|
|
||
| // The same text arriving in a second file is new code there. | ||
| func TestApprovalKeyIsPerFile(t *testing.T) { | ||
| const approved = `diff --git a/one.go b/one.go | ||
| --- a/one.go | ||
| +++ b/one.go | ||
| @@ -1,0 +2 @@ | ||
| + audit(user)` | ||
| const current = `diff --git a/two.go b/two.go | ||
| --- a/two.go | ||
| +++ b/two.go | ||
| @@ -1,0 +2 @@ | ||
| + audit(user)` | ||
|
|
||
| got, err := changesSince(changesSinceContext{ | ||
| newerDiff: parseDiffOrFail(t, current), | ||
| olderDiff: parseDiffOrFail(t, approved), | ||
| normalizer: newNormalizer(retention(true)), | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("changesSince: %v", err) | ||
| } | ||
| if len(got) != 1 { | ||
| t.Fatalf("the same line in a different file is new there, got %d files", len(got)) | ||
| } | ||
| } | ||
|
|
||
| // The raw hash reads the two sides interleaved, so a key built from them | ||
| // separately is looser and must not exist at all while every flag is off. | ||
| func TestNoApprovalKeyWhenNoFlagsEnabled(t *testing.T) { | ||
| hunk := &diff.Hunk{Body: []byte("+foo()\n-bar()")} | ||
| swapped := &diff.Hunk{Body: []byte("-bar()\n+foo()")} | ||
|
|
||
| if hunkHash(hunk) == hunkHash(swapped) { | ||
| t.Fatal("hunkHash is expected to read the two sides interleaved") | ||
| } | ||
| for _, r := range []*owners.ApprovalRetention{nil, {Enabled: false}, retention(false)} { | ||
| n := newNormalizer(r) | ||
| if _, ok := n.approvalKey("x.go", hunk); ok { | ||
| t.Errorf("approvalKey offered a key with no steps enabled: %+v", r) | ||
| } | ||
| } | ||
| if _, ok := newNormalizer(retention(true)).approvalKey("x.go", hunk); !ok { | ||
| t.Error("approvalKey withheld a key with a step enabled") | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,23 @@ func (n normalizer) isTrivial(fileName string, hunk *diff.Hunk) bool { | |
| return n.normalize(fileName, added) == n.normalize(fileName, removed) | ||
| } | ||
|
|
||
| // approvalKey identifies a hunk by what it says once the enabled flags' noise is | ||
| // stripped, so it matches an approval-time counterpart whose bytes differ. | ||
| func (n normalizer) approvalKey(fileName string, hunk *diff.Hunk) (string, bool) { | ||
| // No key at all, so de-duplication is unchanged while every flag is off. | ||
| if len(n.steps) == 0 { | ||
| return "", false | ||
| } | ||
| added, removed, ok := hunkBlocks(hunk.Body) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if !ok { | ||
| return "", false | ||
| } | ||
| // The file name joins the key, unlike in the raw hash: a normalized body says | ||
| // less than a raw one, so identity should not reach across files as far. | ||
| return fileName + "\x00" + n.normalize(fileName, added) + | ||
| "\x00" + n.normalize(fileName, removed), true | ||
| } | ||
|
|
||
| func (n normalizer) normalize(fileName, block string) string { | ||
| for _, step := range n.steps { | ||
| block = step(fileName, block) | ||
|
|
||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent a potential nil pointer dereference panic, we should defensively check if
disnilbefore callingdiffToFilename(d).