[presubmit] Patches should use --default-prefix and similar opts (#36157)

The patching machinery in Brave usually creates patches with the
equivalent of:

```
git -C .. diff --src-prefix=a/ --dst-prefix=b/ --default-prefix --full-index --ignore-space-at-eol browser/foo.cc
```

This is not enforced officially, and every now and then someone
generates patches manually and gets them merged with some other type of
header, which ends up causing issues when syncing, or using other types
of tooling that assume certain expectations from these patches. This PR
prevents that.

Another check being added is to validate that a patch added to this path
always have one file only in it, as this is an invariant of our patching
system.

Resolves https://github.com/brave/brave-browser/issues/55231
This commit is contained in:
cdesouza-chromium
2026-05-05 21:29:53 +01:00
committed by GitHub
parent 9366305eaa
commit b7e667cb83
2 changed files with 293 additions and 3 deletions
+98
View File
@@ -5,8 +5,13 @@
import chromium_presubmit_overrides import chromium_presubmit_overrides
import os import os
import re
# Full 40-char SHA hashes as produced by `git diff --full-index`.
_FULL_INDEX_RE = re.compile(r'^index ([0-9a-f]{40})\.\.([0-9a-f]{40})( \d+)?$')
PRESUBMIT_VERSION = '2.0.0' PRESUBMIT_VERSION = '2.0.0'
TEST_FILE_PATTERN = [r'.+_test\.py$']
# Adds support for chromium_presubmit_config.json5 and some helpers. # Adds support for chromium_presubmit_config.json5 and some helpers.
@@ -15,6 +20,11 @@ def CheckToModifyInputApi(input_api, _output_api):
return [] return []
def CheckTests(input_api, output_api):
return input_api.canned_checks.RunUnitTestsInDirectory(
input_api, output_api, '.', files_to_check=TEST_FILE_PATTERN)
def CheckPatchFile(input_api, output_api): def CheckPatchFile(input_api, output_api):
files_to_check = (r'.+\.patch$', ) files_to_check = (r'.+\.patch$', )
files_to_skip = () files_to_skip = ()
@@ -98,3 +108,91 @@ def CheckPatchFileSource(input_api, output_api):
output_api.PresubmitError( output_api.PresubmitError(
'Patch filename does not match source file in diff header', items) 'Patch filename does not match source file in diff header', items)
] ]
def CheckPatchFileIndexHeader(input_api, output_api):
"""Checks that patch files use full SHA hashes in the index line.
Patches must be generated with `git diff --full-index` so that index lines
contain full 40-character SHA hashes rather than abbreviated ones.
Expected header format:
diff --git a/path/to/file.cc b/path/to/file.cc
index a1b2c3d4e5f6...(40 hex chars)..f6e5d4c3b2a1 100644
--- a/path/to/file.cc
+++ b/path/to/file.cc
"""
files_to_check = (r'.+\.patch$', )
files_to_skip = ()
file_filter = lambda f: input_api.FilterSourceFile(
f, files_to_check=files_to_check, files_to_skip=files_to_skip)
items = []
for f in input_api.AffectedSourceFiles(file_filter):
contents = list(f.NewContents())
if not contents:
continue
# The index line should appear within the first few header lines.
index_line = None
for i, line in enumerate(contents[:5]):
if line.startswith('index '):
index_line = (i + 1, line)
break
if index_line is None:
items.append(
f'{f.LocalPath()}: missing index line in patch header')
continue
lineno, line = index_line
if not _FULL_INDEX_RE.match(line):
items.append(f'{f.LocalPath()}:{lineno}: index line must use full'
' 40-character SHA hashes (generate patches with'
' `git diff --full-index`).\n'
f' Found: {line}')
if not items:
return []
return [
output_api.PresubmitError(
'Patch index line does not use full SHA hashes', items)
]
def CheckPatchFileSingleSource(input_api, output_api):
"""Checks that each patch file patches exactly one source file.
Each .patch file must contain a single `diff --git` section. Patching
multiple files in one .patch file is not allowed; use one patch file per
source file instead.
"""
files_to_check = (r'.+\.patch$', )
files_to_skip = ()
file_filter = lambda f: input_api.FilterSourceFile(
f, files_to_check=files_to_check, files_to_skip=files_to_skip)
items = []
for f in input_api.AffectedSourceFiles(file_filter):
contents = list(f.NewContents())
diff_headers = [(i + 1, line) for i, line in enumerate(contents)
if line.startswith('diff --git ')]
if len(diff_headers) != 1:
count = len(diff_headers)
detail = (f' Found {count} diff --git headers'
if count > 1 else ' No diff --git header found')
items.append(f'{f.LocalPath()}: patch must contain exactly one'
f' source file.\n{detail}')
if not items:
return []
return [
output_api.PresubmitError(
'Patch file must patch exactly one source file', items)
]
+195 -3
View File
@@ -10,7 +10,7 @@ import brave_chromium_utils
import PRESUBMIT import PRESUBMIT
with brave_chromium_utils.sys_path("//"): with brave_chromium_utils.sys_path("//"):
from PRESUBMIT_test_mocks import MockFile, MockAffectedFile from PRESUBMIT_test_mocks import MockAffectedFile
from PRESUBMIT_test_mocks import MockInputApi, MockOutputApi from PRESUBMIT_test_mocks import MockInputApi, MockOutputApi
@@ -134,8 +134,200 @@ class PatchFileTest(unittest.TestCase):
actual_errors = len(results) actual_errors = len(results)
self.assertEqual( self.assertEqual(
case['expected_errors'], actual_errors, case['expected_errors'], actual_errors,
f"Expected {case['expected_errors']} errors, got {actual_errors}" f"Expected {case['expected_errors']} errors,"
) f" got {actual_errors}")
class PatchFileSourceTest(unittest.TestCase):
def _run(self, contents, filename):
affected_file = MockAffectedFile(filename, contents)
input_api = MockInputApi()
input_api.files = [affected_file]
return PRESUBMIT.CheckPatchFileSource(input_api, MockOutputApi())
def testFilenameMatchesConventionPasses(self):
# base/metrics/foo.h -> base-metrics-foo.h.patch
results = self._run(
contents=['diff --git a/base/metrics/foo.h b/base/metrics/foo.h'],
filename='base-metrics-foo.h.patch')
self.assertEqual(0, len(results))
def testFilenameDeepPathPasses(self):
# chrome/browser/ui/views/frame/bar.cc
# -> chrome-browser-ui-views-frame-bar.cc.patch
results = self._run(
contents=[
'diff --git a/chrome/browser/ui/views/frame/bar.cc'
' b/chrome/browser/ui/views/frame/bar.cc'
],
filename='chrome-browser-ui-views-frame-bar.cc.patch')
self.assertEqual(0, len(results))
def testWrongFilenameFails(self):
results = self._run(
contents=['diff --git a/base/metrics/foo.h b/base/metrics/foo.h'],
filename='wrong-name.patch')
self.assertEqual(1, len(results))
def testMissingDirectoryInFilenameFails(self):
# Header says base/metrics/foo.h but filename omits the directory level.
results = self._run(
contents=['diff --git a/base/metrics/foo.h b/base/metrics/foo.h'],
filename='base-foo.h.patch')
self.assertEqual(1, len(results))
def testEmptyPatchFails(self):
results = self._run(contents=[], filename='base-foo.h.patch')
self.assertEqual(1, len(results))
def testMissingDiffHeaderFails(self):
results = self._run(contents=['--- a/base/foo.h', '+++ b/base/foo.h'],
filename='base-foo.h.patch')
self.assertEqual(1, len(results))
def testNonPatchFileIgnored(self):
results = self._run(contents=['diff --git a/base/foo.h b/base/foo.h'],
filename='base-foo.h.txt')
self.assertEqual(0, len(results))
_FULL_SHA = 'a' * 40
_FULL_SHA2 = 'b' * 40
_SHORT_SHA = 'abc1234'
_SHORT_SHA2 = 'def5678'
class PatchFileIndexHeaderTest(unittest.TestCase):
def _run(self, contents, filename='foo-bar.patch'):
affected_file = MockAffectedFile(filename, contents)
input_api = MockInputApi()
input_api.files = [affected_file]
return PRESUBMIT.CheckPatchFileIndexHeader(input_api, MockOutputApi())
def testValidFullIndexHeader(self):
results = self._run([
'diff --git a/foo/bar.cc b/foo/bar.cc',
f'index {_FULL_SHA}..{_FULL_SHA2} 100644',
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
'@@ -1 +1 @@',
'+// brave',
])
self.assertEqual(0, len(results))
def testAbbreviatedShaFails(self):
results = self._run([
'diff --git a/foo/bar.cc b/foo/bar.cc',
f'index {_SHORT_SHA}..{_SHORT_SHA2} 100644',
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
])
self.assertEqual(1, len(results))
def testMissingIndexLineFails(self):
results = self._run([
'diff --git a/foo/bar.cc b/foo/bar.cc',
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
])
self.assertEqual(1, len(results))
def testEmptyPatchSkipped(self):
results = self._run([])
self.assertEqual(0, len(results))
def testExecutableModeInIndexLine(self):
results = self._run([
'diff --git a/foo/bar.py b/foo/bar.py',
f'index {_FULL_SHA}..{_FULL_SHA2} 100755',
'--- a/foo/bar.py',
'+++ b/foo/bar.py',
])
self.assertEqual(0, len(results))
def testIndexLineWithoutMode(self):
# Some git diff outputs omit the mode when it doesn't change.
results = self._run([
'diff --git a/foo/bar.cc b/foo/bar.cc',
f'index {_FULL_SHA}..{_FULL_SHA2}',
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
])
self.assertEqual(0, len(results))
def testNonPatchFileIgnored(self):
affected_file = MockAffectedFile('foo.txt', [
'diff --git a/foo/bar.cc b/foo/bar.cc',
f'index {_SHORT_SHA}..{_SHORT_SHA2} 100644',
])
input_api = MockInputApi()
input_api.files = [affected_file]
results = PRESUBMIT.CheckPatchFileIndexHeader(input_api,
MockOutputApi())
self.assertEqual(0, len(results))
class PatchFileSingleSourceTest(unittest.TestCase):
def _run(self, contents, filename='foo-bar.patch'):
affected_file = MockAffectedFile(filename, contents)
input_api = MockInputApi()
input_api.files = [affected_file]
return PRESUBMIT.CheckPatchFileSingleSource(input_api, MockOutputApi())
def testSingleSourcePasses(self):
results = self._run([
'diff --git a/foo/bar.cc b/foo/bar.cc',
f'index {_FULL_SHA}..{_FULL_SHA2} 100644',
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
'@@ -1 +1 @@',
'+// brave',
])
self.assertEqual(0, len(results))
def testMultipleSourcesFails(self):
results = self._run([
'diff --git a/foo/bar.cc b/foo/bar.cc',
f'index {_FULL_SHA}..{_FULL_SHA2} 100644',
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
'@@ -1 +1 @@',
'+// brave',
'diff --git a/foo/baz.cc b/foo/baz.cc',
f'index {_FULL_SHA}..{_FULL_SHA2} 100644',
'--- a/foo/baz.cc',
'+++ b/foo/baz.cc',
'@@ -1 +1 @@',
'+// also brave',
])
self.assertEqual(1, len(results))
def testNoDiffHeaderFails(self):
results = self._run([
'--- a/foo/bar.cc',
'+++ b/foo/bar.cc',
'@@ -1 +1 @@',
'+// brave',
])
self.assertEqual(1, len(results))
def testEmptyPatchFails(self):
results = self._run([])
self.assertEqual(1, len(results))
def testNonPatchFileIgnored(self):
affected_file = MockAffectedFile('foo.txt', [
'diff --git a/foo/bar.cc b/foo/bar.cc',
'diff --git a/foo/baz.cc b/foo/baz.cc',
])
input_api = MockInputApi()
input_api.files = [affected_file]
results = PRESUBMIT.CheckPatchFileSingleSource(input_api,
MockOutputApi())
self.assertEqual(0, len(results))
if __name__ == '__main__': if __name__ == '__main__':