pre-upload: Verify the bug reference based on the upstream project.
When uploading patches to AOSP directly from a Chromium OS checkout, we
need to specify the bug number in a different format. This patch checks
the upstream project the branch is tracking and decides what format
should be used for the bug number line.
BUG=None
TEST=Added unittests.
Change-Id: I2084b4de7c55051bb229f1642384788916d1253b
Reviewed-on: https://chromium-review.googlesource.com/297173
Commit-Ready: Alex Deymo <deymo@chromium.org>
Tested-by: Alex Deymo <deymo@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/pre-upload.py b/pre-upload.py
index 5432d40..40bc5c7 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -10,6 +10,7 @@
from __future__ import print_function
+import collections
import ConfigParser
import fnmatch
import functools
@@ -97,6 +98,9 @@
# General Helpers
+Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
+
+
# pylint: disable=redefined-builtin
def _run_command(cmd, cwd=None, input=None,
redirect_stderr=False, combine_stdout_stderr=False):
@@ -490,7 +494,7 @@
return HookFailure(msg, [example, str(ex)])
-def _check_change_has_bug_field(_project, commit):
+def _check_change_has_bug_field(project, commit):
"""Check for a correctly formatted 'BUG=' field in the commit message."""
OLD_BUG_RE = r'\nBUG=.*chromium-os'
if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
@@ -498,15 +502,29 @@
'the chromium tracker in your BUG= line now.')
return HookFailure(msg)
- BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium|brillo|b):\d+)'
- if not re.search(BUG_RE, _get_commit_desc(commit)):
- msg = ('Changelist description needs BUG field (after first line):\n'
- 'BUG=brillo:9999 (for Brillo tracker)\n'
- 'BUG=chromium:9999 (for public tracker)\n'
- 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
- 'BUG=b:9999 (for buganizer)\n'
- 'BUG=None')
- return HookFailure(msg)
+ # Android internal and external projects use "Bug: " to track bugs in
+ # buganizer.
+ BUG_COLON_REMOTES = (
+ 'aosp',
+ 'goog',
+ )
+ if project.remote in BUG_COLON_REMOTES:
+ BUG_RE = r'\nBug: ?([Nn]one|\d+)'
+ if not re.search(BUG_RE, _get_commit_desc(commit)):
+ msg = ('Changelist description needs BUG field (after first line):\n'
+ 'Bug: 9999 (for buganizer)\n'
+ 'BUG=None')
+ return HookFailure(msg)
+ else:
+ BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium|brillo|b):\d+)'
+ if not re.search(BUG_RE, _get_commit_desc(commit)):
+ msg = ('Changelist description needs BUG field (after first line):\n'
+ 'BUG=brillo:9999 (for Brillo tracker)\n'
+ 'BUG=chromium:9999 (for public tracker)\n'
+ 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
+ 'BUG=b:9999 (for buganizer)\n'
+ 'BUG=None')
+ return HookFailure(msg)
def _check_for_uprev(project, commit, project_top=None):
@@ -532,7 +550,7 @@
still better off than without this check.
Args:
- project: The project to look at
+ project: The Project to look at
commit: The commit to look at
project_top: Top dir to process commits in
@@ -545,7 +563,7 @@
whitelist = (
'chromiumos/overlays/portage-stable',
)
- if project in whitelist:
+ if project.name in whitelist:
return None
def FinalName(obj):
@@ -628,7 +646,7 @@
have less builtin error checking.
Args:
- project: The project to look at
+ project: The Project to look at
commit: The commit to look at
Returns:
@@ -640,7 +658,7 @@
whitelist = (
'chromiumos/overlays/portage-stable',
)
- if project in whitelist:
+ if project.name in whitelist:
return None
BAD_EAPIS = ('0', '1', '2', '3')
@@ -697,7 +715,7 @@
KEYWORDS="-* ..." # Is known to only work on specific arches.
Args:
- project: The project to look at
+ project: The Project to look at
commit: The commit to look at
Returns:
@@ -773,12 +791,12 @@
whitelist = (
'chromiumos/overlays/portage-stable',
)
- if project in whitelist:
+ if project.name in whitelist:
return None
# We assume the repo name is the same as the dir name on disk.
# It would be dumb to not have them match though.
- project = os.path.basename(project)
+ project_base = os.path.basename(project.name)
is_variant = lambda x: x.startswith('overlay-variant-')
is_board = lambda x: x.startswith('overlay-')
@@ -796,7 +814,7 @@
if m:
overlay = m.group(1)
if not overlay or not is_board(overlay):
- overlay = project
+ overlay = project_base
pv = m.group(3).split('-', 1)[0]
@@ -1161,7 +1179,7 @@
write an error message to stdout.
"""
env = dict(os.environ)
- env['PRESUBMIT_PROJECT'] = project
+ env['PRESUBMIT_PROJECT'] = project.name
env['PRESUBMIT_COMMIT'] = commit
# Put affected files in an environment variable
@@ -1370,12 +1388,12 @@
return hooks
-def _run_project_hooks(project, proj_dir=None,
+def _run_project_hooks(project_name, proj_dir=None,
commit_list=None, presubmit=False):
"""For each project run its project specific hook from the hooks dictionary.
Args:
- project: The name of project to run hooks for.
+ project_name: The name of project to run hooks for.
proj_dir: If non-None, this is the directory the project is in. If None,
we'll ask repo.
commit_list: A list of commits to run hooks against. If None or empty list
@@ -1386,13 +1404,14 @@
Boolean value of whether any errors were ecountered while running the hooks.
"""
if proj_dir is None:
- proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
+ proj_dirs = _run_command(
+ ['repo', 'forall', project_name, '-c', 'pwd']).split()
if len(proj_dirs) == 0:
- print('%s cannot be found.' % project, file=sys.stderr)
+ print('%s cannot be found.' % project_name, file=sys.stderr)
print('Please specify a valid project.', file=sys.stderr)
return True
if len(proj_dirs) > 1:
- print('%s is associated with multiple directories.' % project,
+ print('%s is associated with multiple directories.' % project_name,
file=sys.stderr)
print('Please specify a directory to help disambiguate.', file=sys.stderr)
return True
@@ -1402,15 +1421,26 @@
# hooks assume they are run from the root of the project
os.chdir(proj_dir)
+ remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
+ '--symbolic-full-name', '@{u}']).strip()
+ if not remote_branch:
+ print('Your project %s doesn\'t track any remote repo.' % project_name,
+ file=sys.stderr)
+ remote = None
+ else:
+ remote, _branch = remote_branch.split('/', 1)
+
+ project = Project(name=project_name, dir=proj_dir, remote=remote)
+
if not commit_list:
try:
commit_list = _get_commits()
except VerifyException as e:
- PrintErrorForProject(project, HookFailure(str(e)))
+ PrintErrorForProject(project.name, HookFailure(str(e)))
os.chdir(pwd)
return True
- hooks = _get_project_hooks(project, presubmit)
+ hooks = _get_project_hooks(project.name, presubmit)
error_found = False
for commit in commit_list:
error_list = []
@@ -1420,7 +1450,7 @@
error_list.append(hook_error)
error_found = True
if error_list:
- PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
+ PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
error_list)
os.chdir(pwd)
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 890a3e5..d11ee3e 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -32,6 +32,11 @@
pre_upload = __import__('pre-upload')
+def ProjectNamed(project_name):
+ """Wrapper to create a Project with just the name"""
+ return pre_upload.Project(project_name, None, None)
+
+
class TryUTF8DecodeTest(cros_test_lib.TestCase):
"""Verify we sanely handle unicode content."""
@@ -64,7 +69,7 @@
(7, u"# define " + (u"x" * 80)), # OK (compiler directive)
(8, u"#define" + (u"x" * 74)), # Too long
]
- failure = pre_upload._check_no_long_lines('PROJECT', 'COMMIT')
+ failure = pre_upload._check_no_long_lines(ProjectNamed('PROJECT'), 'COMMIT')
self.assertTrue(failure)
self.assertEquals('Found lines longer than 80 characters (first 5 shown):',
failure.msg)
@@ -94,7 +99,8 @@
"""Report an error when the prefix doesn't match the base directory."""
self.file_mock.return_value = ['foo/foo.cc', 'foo/subdir/baz.cc']
self.desc_mock.return_value = 'bar: Some commit'
- failure = pre_upload._check_project_prefix('PROJECT', 'COMMIT')
+ failure = pre_upload._check_project_prefix(ProjectNamed('PROJECT'),
+ 'COMMIT')
self.assertTrue(failure)
self.assertEquals(('The commit title for changes affecting only foo' +
' should start with "foo: "'), failure.msg)
@@ -103,14 +109,16 @@
"""Use a prefix that matches the base directory."""
self.file_mock.return_value = ['foo/foo.cc', 'foo/subdir/baz.cc']
self.desc_mock.return_value = 'foo: Change some files.'
- self.assertFalse(pre_upload._check_project_prefix('PROJECT', 'COMMIT'))
+ self.assertFalse(
+ pre_upload._check_project_prefix(ProjectNamed('PROJECT'), 'COMMIT'))
def testAliasFile(self):
"""Use .project_alias to override the project name."""
self._WriteAliasFile('foo/.project_alias', 'project')
self.file_mock.return_value = ['foo/foo.cc', 'foo/subdir/bar.cc']
self.desc_mock.return_value = 'project: Use an alias.'
- self.assertFalse(pre_upload._check_project_prefix('PROJECT', 'COMMIT'))
+ self.assertFalse(
+ pre_upload._check_project_prefix(ProjectNamed('PROJECT'), 'COMMIT'))
def testAliasFileWithSubdirs(self):
"""Check that .project_alias is used when only modifying subdirectories."""
@@ -121,7 +129,8 @@
'foo/subdir/blah/baz.cc'
]
self.desc_mock.return_value = 'project: Alias with subdirs.'
- self.assertFalse(pre_upload._check_project_prefix('PROJECT', 'COMMIT'))
+ self.assertFalse(
+ pre_upload._check_project_prefix(ProjectNamed('PROJECT'), 'COMMIT'))
class CheckKernelConfig(cros_test_lib.MockTestCase):
@@ -208,7 +217,7 @@
class CheckEbuildEapi(cros_test_lib.MockTestCase):
"""Tests for _check_ebuild_eapi."""
- PORTAGE_STABLE = 'chromiumos/overlays/portage-stable'
+ PORTAGE_STABLE = ProjectNamed('chromiumos/overlays/portage-stable')
def setUp(self):
self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
@@ -230,18 +239,19 @@
self.content_mock.side_effect = Exception()
self.file_mock.return_value = ['some-file', 'ebuild/dir', 'an.ebuild~']
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
# Make sure our condition above triggers.
self.file_mock.return_value.append('a/real.ebuild')
- self.assertRaises(Exception, pre_upload._check_ebuild_eapi, 'o', 'HEAD')
+ self.assertRaises(Exception, pre_upload._check_ebuild_eapi,
+ ProjectNamed('o'), 'HEAD')
def testSkipSymlink(self):
"""Skip files that are just symlinks."""
self.file_mock.return_value = ['a-r1.ebuild']
self.content_mock.return_value = 'a.ebuild'
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
def testRejectEapiImplicit0Content(self):
@@ -252,7 +262,7 @@
IUSE="foo"
src_compile() { }
"""
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertTrue(isinstance(ret, errors.HookFailure))
def testRejectExplicitEapi1Content(self):
@@ -266,17 +276,17 @@
"""
# Make sure we only check the first EAPI= setting.
self.content_mock.return_value = template % '1\nEAPI=4'
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertTrue(isinstance(ret, errors.HookFailure))
# Verify we handle double quotes too.
self.content_mock.return_value = template % '"1"'
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertTrue(isinstance(ret, errors.HookFailure))
# Verify we handle single quotes too.
self.content_mock.return_value = template % "'1'"
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertTrue(isinstance(ret, errors.HookFailure))
def testAcceptExplicitEapi4Content(self):
@@ -290,17 +300,17 @@
"""
# Make sure we only check the first EAPI= setting.
self.content_mock.return_value = template % '4\nEAPI=1'
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
# Verify we handle double quotes too.
self.content_mock.return_value = template % '"5"'
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
# Verify we handle single quotes too.
self.content_mock.return_value = template % "'5-hdepend'"
- ret = pre_upload._check_ebuild_eapi('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_eapi(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
@@ -315,7 +325,7 @@
"""If no ebuilds are found, do not scan."""
self.file_mock.return_value = ['a.file', 'ebuild-is-not.foo']
- ret = pre_upload._check_ebuild_keywords('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_keywords(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
self.assertEqual(self.content_mock.call_count, 0)
@@ -325,7 +335,7 @@
self.file_mock.return_value = ['a.file', 'blah', 'foo.ebuild', 'cow']
self.content_mock.return_value = ''
- ret = pre_upload._check_ebuild_keywords('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_keywords(ProjectNamed('overlay'), 'HEAD')
self.assertEqual(ret, None)
self.assertEqual(self.content_mock.call_count, 1)
@@ -340,7 +350,7 @@
self.file_mock.return_value = ['a.ebuild']
self.content_mock.return_value = content
- ret = pre_upload._check_ebuild_keywords('overlay', 'HEAD')
+ ret = pre_upload._check_ebuild_keywords(ProjectNamed('overlay'), 'HEAD')
if fails:
self.assertTrue(isinstance(ret, errors.HookFailure))
else:
@@ -376,12 +386,12 @@
class CheckEbuildVirtualPv(cros_test_lib.MockTestCase):
"""Tests for _check_ebuild_virtual_pv."""
- PORTAGE_STABLE = 'chromiumos/overlays/portage-stable'
- CHROMIUMOS_OVERLAY = 'chromiumos/overlays/chromiumos'
- BOARD_OVERLAY = 'chromiumos/overlays/board-overlays'
- PRIVATE_OVERLAY = 'chromeos/overlays/overlay-link-private'
- PRIVATE_VARIANT_OVERLAY = ('chromeos/overlays/'
- 'overlay-variant-daisy-spring-private')
+ PORTAGE_STABLE = ProjectNamed('chromiumos/overlays/portage-stable')
+ CHROMIUMOS_OVERLAY = ProjectNamed('chromiumos/overlays/chromiumos')
+ BOARD_OVERLAY = ProjectNamed('chromiumos/overlays/board-overlays')
+ PRIVATE_OVERLAY = ProjectNamed('chromeos/overlays/overlay-link-private')
+ PRIVATE_VARIANT_OVERLAY = ProjectNamed('chromeos/overlays/'
+ 'overlay-variant-daisy-spring-private')
def setUp(self):
self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
@@ -389,7 +399,7 @@
def testNoVirtuals(self):
"""Skip non virtual packages."""
self.file_mock.return_value = ['some/package/package-3.ebuild']
- ret = pre_upload._check_ebuild_virtual_pv('overlay', 'H')
+ ret = pre_upload._check_ebuild_virtual_pv(ProjectNamed('overlay'), 'H')
self.assertEqual(ret, None)
def testCommonVirtuals(self):
@@ -663,13 +673,15 @@
# enable it for all the call sites below.
return 1 # pylint: disable=W0101
- def assertMessageAccepted(self, msg, project='project', commit='1234'):
+ def assertMessageAccepted(self, msg, project=ProjectNamed('project'),
+ commit='1234'):
"""Assert _check_change_has_bug_field accepts |msg|."""
self.msg_mock.return_value = msg
ret = self.CheckMessage(project, commit)
self.assertEqual(ret, None)
- def assertMessageRejected(self, msg, project='project', commit='1234'):
+ def assertMessageRejected(self, msg, project=ProjectNamed('project'),
+ commit='1234'):
"""Assert _check_change_has_bug_field rejects |msg|."""
self.msg_mock.return_value = msg
ret = self.CheckMessage(project, commit)
@@ -679,44 +691,60 @@
class CheckCommitMessageBug(CommitMessageTestCase):
"""Tests for _check_change_has_bug_field."""
+ AOSP_PROJECT = pre_upload.Project(name='overlay', dir='', remote='aosp')
+ CROS_PROJECT = pre_upload.Project(name='overlay', dir='', remote='cros')
+
@staticmethod
def CheckMessage(project, commit):
return pre_upload._check_change_has_bug_field(project, commit)
def testNormal(self):
"""Accept a commit message w/a valid BUG."""
- self.assertMessageAccepted('\nBUG=chromium:1234\n')
- self.assertMessageAccepted('\nBUG=chrome-os-partner:1234\n')
- self.assertMessageAccepted('\nBUG=b:1234\n')
+ self.assertMessageAccepted('\nBUG=chromium:1234\n', self.CROS_PROJECT)
+ self.assertMessageAccepted('\nBUG=chrome-os-partner:1234\n',
+ self.CROS_PROJECT)
+ self.assertMessageAccepted('\nBUG=b:1234\n', self.CROS_PROJECT)
+
+ self.assertMessageAccepted('\nBug: 1234\n', self.AOSP_PROJECT)
+ self.assertMessageAccepted('\nBug:1234\n', self.AOSP_PROJECT)
def testNone(self):
"""Accept BUG=None."""
- self.assertMessageAccepted('\nBUG=None\n')
- self.assertMessageAccepted('\nBUG=none\n')
- self.assertMessageRejected('\nBUG=NONE\n')
+ self.assertMessageAccepted('\nBUG=None\n', self.CROS_PROJECT)
+ self.assertMessageAccepted('\nBUG=none\n', self.CROS_PROJECT)
+ self.assertMessageAccepted('\nBug: None\n', self.AOSP_PROJECT)
+ self.assertMessageAccepted('\nBug:none\n', self.AOSP_PROJECT)
+
+ for project in (self.AOSP_PROJECT, self.CROS_PROJECT):
+ self.assertMessageRejected('\nBUG=NONE\n', project)
def testBlank(self):
"""Reject blank values."""
- self.assertMessageRejected('\nBUG=\n')
- self.assertMessageRejected('\nBUG= \n')
+ for project in (self.AOSP_PROJECT, self.CROS_PROJECT):
+ self.assertMessageRejected('\nBUG=\n', project)
+ self.assertMessageRejected('\nBUG= \n', project)
+ self.assertMessageRejected('\nBug:\n', project)
+ self.assertMessageRejected('\nBug: \n', project)
def testNotFirstLine(self):
"""Reject the first line."""
- self.assertMessageRejected('BUG=None\n\n\n')
+ for project in (self.AOSP_PROJECT, self.CROS_PROJECT):
+ self.assertMessageRejected('BUG=None\n\n\n', project)
def testNotInline(self):
"""Reject not at the start of line."""
- self.assertMessageRejected('\n BUG=None\n')
- self.assertMessageRejected('\n\tBUG=None\n')
+ for project in (self.AOSP_PROJECT, self.CROS_PROJECT):
+ self.assertMessageRejected('\n BUG=None\n', project)
+ self.assertMessageRejected('\n\tBUG=None\n', project)
def testOldTrackers(self):
"""Reject commit messages using old trackers."""
- self.assertMessageRejected('\nBUG=chromium-os:1234\n')
+ self.assertMessageRejected('\nBUG=chromium-os:1234\n', self.CROS_PROJECT)
def testNoTrackers(self):
"""Reject commit messages w/invalid trackers."""
- self.assertMessageRejected('\nBUG=booga:1234\n')
- self.assertMessageRejected('\nBUG=br:1234\n')
+ self.assertMessageRejected('\nBUG=booga:1234\n', self.CROS_PROJECT)
+ self.assertMessageRejected('\nBUG=br:1234\n', self.CROS_PROJECT)
def testMissing(self):
"""Reject commit messages w/no BUG line."""
@@ -996,13 +1024,15 @@
def assertAccepted(self, files, project='project', commit='fake sha1'):
"""Assert _check_for_uprev accepts |files|."""
self.file_mock.return_value = self._Files(files)
- ret = pre_upload._check_for_uprev(project, commit, project_top=self.tempdir)
+ ret = pre_upload._check_for_uprev(ProjectNamed(project), commit,
+ project_top=self.tempdir)
self.assertEqual(ret, None)
def assertRejected(self, files, project='project', commit='fake sha1'):
"""Assert _check_for_uprev rejects |files|."""
self.file_mock.return_value = self._Files(files)
- ret = pre_upload._check_for_uprev(project, commit, project_top=self.tempdir)
+ ret = pre_upload._check_for_uprev(ProjectNamed(project), commit,
+ project_top=self.tempdir)
self.assertTrue(isinstance(ret, errors.HookFailure))
def testWhitelistOverlay(self):