Files
cdesouza-chromium 49edc62217 [git-cr] Adding git cr commit --fixup=reassign:hash (#37056)
This is just a shortcut for `brockit.py reassign <hash>`.
2026-06-06 23:19:07 +01:00

414 lines
17 KiB
Python
Executable File

#!/usr/bin/env vpython3
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
"""Integration tests for commit.py.
All tests run cmd.py as a subprocess against a FakeChromiumRepo, which stands
in as the brave checkout that `repository.brave.root` resolves to. Shared
sandbox/runner infrastructure is imported from cmd_test.
"""
import os
import platform
import stat
import subprocess
import sys
import unittest
import _boot # noqa: F401
from cmd_test import (CMD_SCRIPT, _GIT_ENV_OVERRIDES, _Sandbox)
from alias.commit import _ReassignShortcut
# ---------------------------------------------------------------------------
# Tests: flag pass-through
# ---------------------------------------------------------------------------
class TestFlagPassthrough(unittest.TestCase):
"""gc must strip its own flags and forward everything else to git."""
def setUp(self) -> None:
self._sandbox = _Sandbox()
self._sandbox.__enter__()
self._sandbox.install_hook()
def tearDown(self) -> None:
self._sandbox.__exit__(None, None, None)
def test_custom_flags_stripped_before_git(self) -> None:
"""--tagged, --issue, --culprit are stripped; git never receives them.
Each flag is tested in isolation with a staged change and no hook. If
any flag leaked through, git would exit non-zero with 'unknown option'.
A clean exit proves gc removed it from the argv it forwarded.
"""
for flag, value in [
('--tagged', 'WIP'),
('--issue', '42'),
('--culprit', 'deadbeef'),
]:
with self.subTest(flag=flag):
self._sandbox.stage_change(f'content for {flag}\n')
result = self._sandbox.run_gc([
'commit', flag, value, '--no-verify', '-m', f'test {flag}'
])
self.assertEqual(
result.returncode,
0,
msg=f'{flag} may have leaked to git: {result.stderr!r}',
)
def test_unknown_flag_reaches_git(self) -> None:
"""gc forwards unknown flags to git, which then reports the error."""
result = self._sandbox.run_gc(['commit', '--tgas', '-m', 'msg'])
# The error must come from git, not from gc.
self.assertNotEqual(result.returncode, 0)
git_error = result.stderr + result.stdout
self.assertTrue(
'unknown' in git_error or 'error' in git_error,
msg=f'Expected a git error, got: {git_error!r}',
)
def test_no_verify_passes_through(self) -> None:
"""--no-verify reaches git (bypass hook) and the commit succeeds."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(
['commit', '--no-verify', '-m', 'bare commit'])
self.assertEqual(result.returncode, 0)
def test_short_flags_pass_through(self) -> None:
"""Single-dash flags (-m) are forwarded verbatim."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(['commit', '-m', 'short flag test'])
self.assertEqual(result.returncode, 0)
def test_amend_passes_through(self) -> None:
"""--amend is forwarded to git without gc interference."""
# We only care that gc does not produce its own error about --amend.
result = self._sandbox.run_gc(['commit', '--amend', '--no-edit'])
# git may fail (nothing new to amend), but any error is git's, not gc's.
git_error = result.stderr + result.stdout
self.assertNotIn('git_cr:', git_error)
# ---------------------------------------------------------------------------
# Tests: commit integration (requires hook)
# ---------------------------------------------------------------------------
class TestCommitIntegration(unittest.TestCase):
"""gc must inject env vars that the commit-msg hook reads correctly."""
def setUp(self) -> None:
self._sandbox = _Sandbox()
self._sandbox.__enter__()
self._sandbox.install_hook()
def tearDown(self) -> None:
self._sandbox.__exit__(None, None, None)
def test_commit_without_flags_succeeds(self) -> None:
"""A plain commit via gc works end-to-end."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(['commit', '-m', 'Plain commit'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
self.assertIn('Plain commit', self._sandbox.last_commit_message())
def test_tagged_flag_prepends_tag(self) -> None:
"""--tagged WIP results in [WIP] prepended to the commit message."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(
['commit', '--tagged', 'WIP', '-m', 'My change'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
self.assertIn('[WIP]', msg)
self.assertIn('My change', msg)
def test_tagged_multiple_tags(self) -> None:
"""--tagged WIP,CodeHealth adds both [WIP] and [CodeHealth] tags."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(
['commit', '--tagged', 'WIP,CodeHealth', '-m', 'Multi-tag'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
self.assertIn('[WIP]', msg)
self.assertIn('[CodeHealth]', msg)
def test_issue_flag_appends_resolves_link(self) -> None:
"""--issue 1234 appends a Resolves link to the commit message."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(
['commit', '--issue', '1234', '-m', 'Fix something'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
self.assertIn('Resolves', msg)
self.assertIn('1234', msg)
self.assertIn('brave-browser/issues/1234', msg)
def test_issue_multiple_numbers(self) -> None:
"""--issue 11,22 adds Resolves links for both issues."""
self._sandbox.stage_change()
result = self._sandbox.run_gc(
['commit', '--issue', '11,22', '-m', 'Multi-issue fix'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
self.assertIn('issues/11', msg)
self.assertIn('issues/22', msg)
def test_tagged_and_issue_combined(self) -> None:
"""--tagged and --issue can be used together."""
self._sandbox.stage_change()
result = self._sandbox.run_gc([
'commit', '--tagged', 'canary', '--issue', '999', '-m', 'Combined'
])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
self.assertIn('[canary]', msg)
self.assertIn('issues/999', msg)
def test_fixup_commit_skips_hook_processing(self) -> None:
"""fixup! commits bypass tag insertion (hook behaviour)."""
# First commit to have something to fixup.
self._sandbox.stage_change('v1\n')
self._sandbox.run_gc(['commit', '-m', 'Base commit'])
self._sandbox.stage_change('v2\n')
result = self._sandbox.run_gc(
['commit', '--tagged', 'WIP', '-m', 'fixup! Base commit'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
# The hook skips fixup! commits — no tag should be added.
self.assertNotIn('[WIP]', msg)
self.assertTrue(msg.startswith('fixup!'))
def test_empty_message_rejected(self) -> None:
"""A commit with only whitespace in the message is rejected."""
self._sandbox.stage_change()
# Pass the message via a temp file so the hook sees it.
result = self._sandbox.run_gc(['commit', '-m', ' '])
# Either git or the hook rejects this; either way exit != 0.
self.assertNotEqual(result.returncode, 0)
# ---------------------------------------------------------------------------
# Tests: cmd_commit hook sanity check
# ---------------------------------------------------------------------------
class TestCommitSanityCheck(unittest.TestCase):
"""cmd_commit must refuse early when the hook is absent or misconfigured."""
def setUp(self) -> None:
self._sandbox = _Sandbox()
self._sandbox.__enter__()
def tearDown(self) -> None:
self._sandbox.__exit__(None, None, None)
def test_missing_hook_is_rejected(self) -> None:
"""cmd_commit exits non-zero and mentions install-hook when absent."""
# A fresh sandbox has no hook installed.
self._sandbox.stage_change()
result = self._sandbox.run_gc(['commit', '-m', 'should fail'])
self.assertNotEqual(result.returncode, 0)
self.assertIn('install-hook', result.stderr)
@unittest.skipIf(platform.system() == 'Windows',
'executable bit is POSIX-only')
def test_non_executable_hook_is_rejected(self) -> None:
"""cmd_commit exits non-zero when the hook lacks the executable bit."""
# Install as a copy so flipping the exec bit is scoped to the sandbox.
# A symlink would point at the real HOOK_SOURCE and races with other
# tests (in this process or in parallel test files) that chmod it +x.
self._sandbox.install_hook(as_copy=True)
dest = self._sandbox.hook_dest
dest.chmod(dest.stat().st_mode & ~(stat.S_IXUSR | stat.S_IXGRP
| stat.S_IXOTH))
self._sandbox.stage_change()
result = self._sandbox.run_gc(['commit', '-m', 'should fail'])
self.assertNotEqual(result.returncode, 0)
self.assertIn('install-hook', result.stderr)
# ---------------------------------------------------------------------------
# Tests: --fixup=reassign: shortcut (delegates to brockit reassign)
# ---------------------------------------------------------------------------
class TestReassignFixup(unittest.TestCase):
"""git cr commit --fixup=reassign:<ref> delegates to brockit reassign."""
def setUp(self) -> None:
self._sandbox = _Sandbox()
self._sandbox.__enter__()
def tearDown(self) -> None:
self._sandbox.__exit__(None, None, None)
def _commit_count(self) -> int:
return int(
subprocess.check_output(
['git', 'rev-list', '--count', 'HEAD'],
cwd=self._sandbox.root,
text=True,
env={
**os.environ,
**_GIT_ENV_OVERRIDES
},
).strip())
def test_joined_form_creates_reassign_commit(self) -> None:
"""--fixup=reassign:HEAD adds an empty reassign! commit on top.
No hook is installed: the shortcut must work without it because
brockit commits with --no-verify.
"""
before = self._commit_count()
result = self._sandbox.run_gc(['commit', '--fixup=reassign:HEAD'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
self.assertEqual(self._commit_count(), before + 1)
msg = self._sandbox.last_commit_message()
# brockit formats the subject as `reassign!<hash>! <original subject>`.
self.assertTrue(msg.startswith('reassign!'),
msg=f'unexpected message: {msg!r}')
self.assertIn('Add file.txt', msg)
def test_split_form_creates_reassign_commit(self) -> None:
"""The split spelling `--fixup reassign:HEAD` is handled too."""
before = self._commit_count()
result = self._sandbox.run_gc(['commit', '--fixup', 'reassign:HEAD'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
self.assertEqual(self._commit_count(), before + 1)
self.assertTrue(
self._sandbox.last_commit_message().startswith('reassign!'))
def test_empty_ref_is_rejected(self) -> None:
"""--fixup=reassign: with no ref errors out without committing."""
before = self._commit_count()
result = self._sandbox.run_gc(['commit', '--fixup=reassign:'])
self.assertNotEqual(result.returncode, 0)
self.assertIn('reassign', result.stderr)
self.assertEqual(self._commit_count(), before)
def test_extra_passthrough_arg_is_rejected(self) -> None:
"""Mixing the shortcut with a git arg (e.g. -m) errors, no commit."""
before = self._commit_count()
result = self._sandbox.run_gc(
['commit', '--fixup=reassign:HEAD', '-m', 'nope'])
self.assertNotEqual(result.returncode, 0)
self.assertIn('cannot be combined', result.stderr)
self.assertEqual(self._commit_count(), before)
def test_extra_wrapper_flag_is_rejected(self) -> None:
"""Mixing the shortcut with a wrapper flag (--tagged) errors too."""
before = self._commit_count()
result = self._sandbox.run_gc(
['commit', '--tagged', 'WIP', '--fixup=reassign:HEAD'])
self.assertNotEqual(result.returncode, 0)
self.assertIn('cannot be combined', result.stderr)
self.assertIn('--tagged', result.stderr)
self.assertEqual(self._commit_count(), before)
def test_native_fixup_still_passes_through_to_git(self) -> None:
"""A plain `--fixup=<ref>` is not intercepted and reaches git.
git produces a `fixup! Add file.txt` commit, proving the wrapper only
claims the `reassign:` mode and leaves git's own modes alone.
"""
self._sandbox.install_hook()
self._sandbox.stage_change('fixup body\n')
result = self._sandbox.run_gc(
['commit', '--fixup=HEAD', '--no-verify'])
self.assertEqual(result.returncode, 0, msg=f'stderr: {result.stderr}')
msg = self._sandbox.last_commit_message()
self.assertTrue(msg.startswith('fixup!'),
msg=f'unexpected message: {msg!r}')
# ---------------------------------------------------------------------------
# Tests: _ReassignShortcut.from_args detection and leniency (unit)
# ---------------------------------------------------------------------------
class TestReassignShortcutParsing(unittest.TestCase):
"""Unit checks for the lenient arg detection in from_args."""
def test_detects_joined_and_split_forms(self) -> None:
"""Both `--fixup=reassign:X` and `--fixup reassign:X` are recognised."""
self.assertIsNotNone(
_ReassignShortcut.from_args(['--fixup=reassign:HEAD']))
self.assertIsNotNone(
_ReassignShortcut.from_args(['--fixup', 'reassign:HEAD']))
def test_absent_when_no_reassign_marker(self) -> None:
"""Ordinary commits and git's native fixup modes are not claimed."""
self.assertIsNone(_ReassignShortcut.from_args(['--fixup=HEAD']))
self.assertIsNone(_ReassignShortcut.from_args(['-m', 'a message']))
def test_marker_outside_fixup_is_not_the_shortcut(self) -> None:
"""`reassign:` only in a commit message is not the shortcut."""
self.assertIsNone(
_ReassignShortcut.from_args(['-m', 'fix reassign: bug']))
def test_malformed_fixup_does_not_terminate(self) -> None:
"""A `--fixup` with no value must not exit the process.
A strict ArgumentParser would call sys.exit() here; the lenient parser
instead reports "not the shortcut" so the caller falls back to the
normal commit path. `reassign:` appears in another token so the cheap
pre-check passes and the parse is actually attempted.
"""
self.assertIsNone(
_ReassignShortcut.from_args(['reassign:x', '--fixup']))
# ---------------------------------------------------------------------------
# Tests: KeyboardInterrupt / graceful exit
# ---------------------------------------------------------------------------
class TestGracefulExit(unittest.TestCase):
"""gc must not emit Python tracebacks on Ctrl-C."""
def setUp(self) -> None:
self._sandbox = _Sandbox()
self._sandbox.__enter__()
self._sandbox.install_hook()
def tearDown(self) -> None:
self._sandbox.__exit__(None, None, None)
def test_no_traceback_on_keyboard_interrupt(self) -> None:
"""Sending SIGINT produces exit code 130 with no traceback."""
import signal
proc = subprocess.Popen(
[sys.executable,
str(CMD_SCRIPT), 'commit', '-m', 'test'],
cwd=self._sandbox.root,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env={
**os.environ,
**_GIT_ENV_OVERRIDES
},
)
try:
proc.send_signal(signal.SIGINT)
_, stderr = proc.communicate(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
_, stderr = proc.communicate()
self.assertNotIn('Traceback', stderr)
# 130 = Python caught KeyboardInterrupt and called sys.exit(130).
# -2 = OS killed the process with SIGINT before Python handled it.
# Both indicate clean termination; which one occurs is timing-dependent.
self.assertIn(proc.returncode, (-2, 130))
if __name__ == '__main__':
unittest.main()