Files
cdesouza-chromium 9adf57c14b [brockit] Test coverage for Rebase (#36315)
This pr introduces full test coverage for `Rebase`, including
integration tests, in preparation for `rebase --continue`.
2026-05-11 12:10:59 +01:00

717 lines
32 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/.
"""Tests for `brockit.Rebase`.
This file is organised in two layers:
* `RebaseStaticHelpersTest` covers the four `@staticmethod` rebase-todo-file
transformers as pure file-in/file-out unit tests. No git involved.
* `RebaseExecuteTest` covers `Rebase.execute()` end-to-end against a real git
repository tree built by `FakeChromiumRepo`. The editor callbacks
(`GIT_SEQUENCE_EDITOR`, `GIT_EDITOR`) re-enter `brockit.py`'s `__main__`
dispatch via subprocess, exactly as in production -- nothing is mocked out.
"""
import tempfile
import unittest
from dataclasses import dataclass
from pathlib import Path
from unittest.mock import patch
import brockit
from test.fake_chromium_repo import FakeChromiumRepo
class RebaseStaticHelpersTest(unittest.TestCase):
"""Tests for the four todo-file transformers on `Rebase`."""
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self._tmp_root = Path(tmp.name)
def _todo(self, content: str) -> Path:
"""Writes `content` to a fresh todo file and returns its path."""
path = self._tmp_root / 'git-rebase-todo'
path.write_text(content)
return path
# ----- discard_regen_changes_from_rebase_plan ----------------------------
def test_discard_regen_removes_update_patches_line(self):
"""A line whose comment starts with `Update patches from Chromium ` is
dropped; everything else is kept verbatim."""
path = self._todo('pick aaa # [cr148] Real feature commit\n'
'pick bbb # Update patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick ccc # [cr148] Another feature commit\n')
brockit.Rebase.discard_regen_changes_from_rebase_plan(path)
self.assertEqual(
path.read_text(), 'pick aaa # [cr148] Real feature commit\n'
'pick ccc # [cr148] Another feature commit\n')
def test_discard_regen_removes_updated_strings_line(self):
"""A line whose comment starts with `Updated strings for Chromium ` is
dropped; everything else is kept verbatim."""
path = self._todo('pick aaa # [cr148] Real feature commit\n'
'pick bbb # Updated strings for Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick ccc # [cr148] Another feature commit\n')
brockit.Rebase.discard_regen_changes_from_rebase_plan(path)
self.assertEqual(
path.read_text(), 'pick aaa # [cr148] Real feature commit\n'
'pick ccc # [cr148] Another feature commit\n')
def test_discard_regen_preserves_comments_blanks_and_other_picks(self):
"""Comment lines, blank lines, and unrelated picks survive."""
path = self._todo('# Rebase plan generated by git\n'
'\n'
'pick aaa # [cr148] Feature A\n'
'pick bbb # Update patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick ccc # [cr148] Feature B\n'
'\n'
'# Commands:\n')
brockit.Rebase.discard_regen_changes_from_rebase_plan(path)
self.assertEqual(
path.read_text(), '# Rebase plan generated by git\n'
'\n'
'pick aaa # [cr148] Feature A\n'
'pick ccc # [cr148] Feature B\n'
'\n'
'# Commands:\n')
def test_discard_regen_empty_file_is_noop(self):
"""An empty todo file remains empty."""
path = self._todo('')
brockit.Rebase.discard_regen_changes_from_rebase_plan(path)
self.assertEqual(path.read_text(), '')
# ----- recommit_in_rebase_plan -------------------------------------------
def test_recommit_flips_only_first_pick(self):
"""The first occurrence of `pick` becomes `edit`; later `pick`s stay."""
path = self._todo('pick aaa # one\n'
'pick bbb # two\n'
'pick ccc # three\n')
brockit.Rebase.recommit_in_rebase_plan(path)
self.assertEqual(
path.read_text(), 'edit aaa # one\n'
'pick bbb # two\n'
'pick ccc # three\n')
def test_recommit_no_pick_leaves_file_unchanged(self):
"""A todo file with no `pick` lines is untouched."""
path = self._todo('# Just comments\n\n')
brockit.Rebase.recommit_in_rebase_plan(path)
self.assertEqual(path.read_text(), '# Just comments\n\n')
# ----- squash_minor_bumps_from_rebase_plan -------------------------------
def test_squash_collapses_consecutive_version_bumps(self):
"""Repeated `Update from Chromium` commits keep the first as pick and
become squash for the rest, preserving their order."""
path = self._todo(
'pick aaa # Update from Chromium 1.0.0.0 to Chromium 1.0.0.1\n'
'pick bbb # Update from Chromium 1.0.0.1 to Chromium 1.0.0.2\n'
'pick ccc # Update from Chromium 1.0.0.2 to Chromium 1.0.0.3\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(),
'pick aaa # Update from Chromium 1.0.0.0 to Chromium 1.0.0.1\n'
'squash bbb # Update from Chromium 1.0.0.1 to Chromium 1.0.0.2\n'
'squash ccc # Update from Chromium 1.0.0.2 to Chromium 1.0.0.3\n')
def test_squash_reorders_categories(self):
"""The output order is version, plaster reruns, conflict, gnrt, iwyu,
then everything else -- regardless of input order."""
path = self._todo(
'pick aaa # [cr148] Some feature commit\n'
'pick bbb # Conflict-resolved patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick ccc # Update from Chromium 1.0.0.0 to Chromium 1.0.0.1\n'
'pick ddd # [cr148] `gnrt` run for Chromium 1.0.0.1\n'
'pick eee # [cr148] IWYU fixes.\n'
'pick fff # Apply-fixed 🩹 patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(),
'pick ccc # Update from Chromium 1.0.0.0 to Chromium 1.0.0.1\n'
'pick fff # Apply-fixed 🩹 patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick bbb # Conflict-resolved patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick ddd # [cr148] `gnrt` run for Chromium 1.0.0.1\n'
'pick eee # [cr148] IWYU fixes.\n'
'pick aaa # [cr148] Some feature commit\n')
def test_squash_collapses_plaster_reruns(self):
"""First `Apply-fixed patches` stays pick; the rest become squash."""
path = self._todo(
'pick aaa # Apply-fixed 🩹 patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick bbb # Apply-fixed 🩹 patches from Chromium 1.0.0.1 '
'to Chromium 1.0.0.2.\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(),
'pick aaa # Apply-fixed 🩹 patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'squash bbb # Apply-fixed 🩹 patches from Chromium 1.0.0.1 '
'to Chromium 1.0.0.2.\n')
def test_squash_collapses_conflict_resolved(self):
"""Repeated `Conflict-resolved patches` commits keep first as pick."""
path = self._todo(
'pick aaa # Conflict-resolved patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'pick bbb # Conflict-resolved patches from Chromium 1.0.0.1 '
'to Chromium 1.0.0.2.\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(),
'pick aaa # Conflict-resolved patches from Chromium 1.0.0.0 '
'to Chromium 1.0.0.1.\n'
'squash bbb # Conflict-resolved patches from Chromium 1.0.0.1 '
'to Chromium 1.0.0.2.\n')
def test_squash_collapses_gnrt_runs(self):
"""Repeated `gnrt` run commits keep first as pick."""
path = self._todo(
'pick aaa # [cr148] `gnrt` run for Chromium 1.0.0.1\n'
'pick bbb # [cr148] `gnrt` run for Chromium 1.0.0.2\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(),
'pick aaa # [cr148] `gnrt` run for Chromium 1.0.0.1\n'
'squash bbb # [cr148] `gnrt` run for Chromium 1.0.0.2\n')
def test_squash_collapses_iwyu_fixes(self):
"""Repeated `IWYU fixes.` commits keep first as pick."""
path = self._todo('pick aaa # [cr148] IWYU fixes.\n'
'pick bbb # [cr148] IWYU fixes.\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(), 'pick aaa # [cr148] IWYU fixes.\n'
'squash bbb # [cr148] IWYU fixes.\n')
def test_squash_reassign_matched_by_hash_moves_above_target(self):
"""A `reassign!<hash>!` commit is inserted directly above the `pick`
for that hash, and that target line becomes `squash`."""
path = self._todo('pick aaa # [cr148] Feature A\n'
'pick bbb # [cr148] Feature B\n'
'pick zzz # reassign!bbb! [cr148] Feature B\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
# `aaa` and `bbb` are both "others"; reassign for `bbb` moves above it
# and turns `bbb`'s pick into squash.
self.assertEqual(
path.read_text(), 'pick aaa # [cr148] Feature A\n'
'pick zzz # reassign!bbb! [cr148] Feature B\n'
'squash bbb # [cr148] Feature B\n')
def test_squash_reassign_matched_by_message_when_hash_mismatch(self):
"""If the reassign hash doesn't match any pick, fall back to matching
a line whose comment ends with the reassign message."""
path = self._todo(
'pick aaa # [cr148] Feature A\n'
'pick bbb # [cr148] Feature B\n'
# The hash referenced in the reassign (`xxx`) doesn't appear; the
# message `[cr148] Feature B` matches `bbb`'s comment.
'pick zzz # reassign!xxx! [cr148] Feature B\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(
path.read_text(), 'pick aaa # [cr148] Feature A\n'
'pick zzz # reassign!xxx! [cr148] Feature B\n'
'squash bbb # [cr148] Feature B\n')
def test_squash_orphaned_reassign_is_silently_dropped(self):
"""A reassign whose hash and message both fail to match is removed."""
path = self._todo('pick aaa # [cr148] Feature A\n'
'pick zzz # reassign!xxx! [cr148] Some completely '
'unrelated message\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(path.read_text(), 'pick aaa # [cr148] Feature A\n')
def test_squash_reassign_strips_empty_suffix_for_message_lookup(self):
"""An empty reassign carries a trailing ` # empty` marker which must be
stripped before the message-based lookup."""
path = self._todo(
'pick aaa # [cr148] Feature A\n'
'pick bbb # [cr148] Feature B\n'
'pick zzz # reassign!xxx! [cr148] Feature B # empty\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
# The ` # empty` suffix is stripped so `[cr148] Feature B` matches
# `bbb`'s comment.
self.assertEqual(
path.read_text(), 'pick aaa # [cr148] Feature A\n'
'pick zzz # reassign!xxx! [cr148] Feature B # empty\n'
'squash bbb # [cr148] Feature B\n')
def test_squash_drops_comments_and_blank_lines(self):
"""Comment lines and blank lines are not categorised and are absent
from the rewritten file."""
path = self._todo('# A comment\n'
'\n'
'pick aaa # [cr148] Real commit\n'
'\n'
'# Another comment\n')
brockit.Rebase.squash_minor_bumps_from_rebase_plan(path)
self.assertEqual(path.read_text(), 'pick aaa # [cr148] Real commit\n')
# ----- fix_squash_commit_messages ----------------------------------------
def test_fix_squash_standard_keeps_last_valid_line(self):
"""A standard squash commit-message file collapses to just the last
non-comment, non-blank line."""
path = self._todo('# This is the 1st commit message:\n'
'Update from Chromium 1.0.0.0 to Chromium 1.0.0.1\n'
'\n'
'# This is the commit message #2:\n'
'Update from Chromium 1.0.0.1 to Chromium 1.0.0.2\n')
brockit.Rebase.fix_squash_commit_messages(path)
self.assertEqual(path.read_text(),
'Update from Chromium 1.0.0.1 to Chromium 1.0.0.2\n')
def test_fix_squash_reassign_keeps_following_content(self):
"""When the first valid line is `reassign!...`, drop it and keep
everything from the next *valid* line's index onward in the file."""
path = self._todo('# This is the 1st commit message:\n'
'reassign!bbb! [cr148] Feature B\n'
'\n'
'# This is the commit message #2:\n'
'[cr148] Feature B\n'
'\n'
'Original body line.\n')
brockit.Rebase.fix_squash_commit_messages(path)
# The cut starts at the next valid line's *index*, not the line
# before it -- so the `# This is the commit message #2:` comment is
# dropped along with the reassign line. Blanks and content after the
# cut point survive.
self.assertEqual(path.read_text(), '[cr148] Feature B\n'
'\n'
'Original body line.\n')
def test_fix_squash_empty_file_exits(self):
"""A file with only comments and blanks calls `sys.exit`."""
path = self._todo('# Only a comment\n\n# And another\n')
with self.assertRaises(SystemExit):
brockit.Rebase.fix_squash_commit_messages(path)
def test_fix_squash_reassign_with_no_following_content_exits(self):
"""A reassign squash with no content after the `reassign!` line
cannot be collapsed and calls `sys.exit`."""
path = self._todo('# This is the 1st commit message:\n'
'reassign!bbb! [cr148] Feature B\n')
with self.assertRaises(SystemExit):
brockit.Rebase.fix_squash_commit_messages(path)
@dataclass(frozen=True)
class _Scenario:
"""Scenario data returned by `_seed_rebase_scenario` for assertions.
The three `vNNN` fields are commit hashes on master for the version-bump
commits (`Update from Chromium ... to Chromium 1.0.0.N`). `feature` is
the Brave-only commit on the feature branch.
"""
branch: str
v100: str
v101: str
v102: str
feature: str
class RebaseExecuteTest(unittest.TestCase):
"""End-to-end tests for `Rebase.execute()` against a real git tree."""
def setUp(self):
self.repo = FakeChromiumRepo()
self.repo.setup()
self.addCleanup(self.repo.cleanup)
self.repo.create_brave_remote()
self._commit_counter = 0
# ----- Seeding helpers ---------------------------------------------------
def _seed_rebase_scenario(self,
branch_name: str = 'feature-branch'
) -> _Scenario:
"""Builds a minor-version-bump scenario.
Layout produced (commits in order):
initial -> v100 -> v101 (master)
\\-> feature_commit (branch_name, on origin)
\\-> v102 (master continues)
After this returns, the working tree is on `branch_name`.
"""
brave = self.repo.brave
v100 = self.repo.update_brave_version('1.0.0.0')
v101 = self.repo.update_brave_version('1.0.0.1')
self.repo._run_git_command(['checkout', '-b', branch_name], brave)
self.repo.write_and_stage_file('feature.txt', 'feature content\n',
brave)
feature_commit = self.repo.commit('Add brave-only feature.txt', brave)
self.repo._run_git_command(
['push', '--set-upstream', 'origin', branch_name], brave)
self.repo._run_git_command(['checkout', 'master'], brave)
v102 = self.repo.update_brave_version('1.0.0.2')
self.repo._run_git_command(['checkout', branch_name], brave)
return _Scenario(branch=branch_name,
v100=v100,
v101=v101,
v102=v102,
feature=feature_commit)
# ----- Small assertion helpers -------------------------------------------
def _git_log_subjects(self, ref: str = 'HEAD') -> list:
"""Returns the commit subjects from oldest to newest on `ref`."""
out = self.repo._run_git_command(
['log', '--reverse', '--format=%s', ref], self.repo.brave)
return out.splitlines() if out else []
def _git_rev_list_count(self, ref: str = 'HEAD') -> int:
"""Returns the number of commits reachable from `ref`."""
return int(
self.repo._run_git_command(['rev-list', '--count', ref],
self.repo.brave))
def _commit_with_file(self, message: str) -> str:
"""Creates a commit with a small unique file change.
Using a real file change (rather than `--allow-empty`) keeps commits
from being dropped by `git rebase --empty=drop`, which would obscure
what the editor callbacks are actually doing.
"""
self._commit_counter += 1
counter = self._commit_counter
self.repo.write_and_stage_file(f'gen-{counter}.txt',
f'content {counter}\n', self.repo.brave)
return self.repo.commit(message, self.repo.brave)
# ----- Tests -------------------------------------------------------------
def test_execute_happy_path_explicit_refs(self):
"""Explicit `from_ref`/`to_ref` rebases the feature commit onto the
new target; the feature branch's subject is preserved."""
scenario = self._seed_rebase_scenario()
brockit.Rebase().execute(from_ref=scenario.v101,
to_ref=scenario.v102,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
# The feature commit is now on top of v102. We can't compare hashes
# (rebase produces new ones) so we compare subjects and parent.
self.assertEqual(self._git_log_subjects()[-1],
'Add brave-only feature.txt')
parent = self.repo._run_git_command(['rev-parse', 'HEAD^'],
self.repo.brave)
self.assertEqual(parent, scenario.v102)
def test_execute_resolves_to_ref_upstream_label(self):
"""`to_ref=None` resolves to `@upstream` (`origin/<branch>`); since
local and origin agree, the rebase is a no-op and the branch HEAD
remains the feature commit."""
scenario = self._seed_rebase_scenario()
head_before = self.repo._run_git_command(['rev-parse', 'HEAD'],
self.repo.brave)
brockit.Rebase().execute(from_ref=scenario.v101,
to_ref=None,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
head_after = self.repo._run_git_command(['rev-parse', 'HEAD'],
self.repo.brave)
self.assertEqual(head_after, head_before)
def test_execute_invalid_from_ref_raises(self):
"""A from_ref that resolves to nothing is rejected before git runs."""
self._seed_rebase_scenario()
with self.assertRaises(brockit.InvalidInputException):
brockit.Rebase().execute(from_ref='not-a-real-ref-name',
to_ref='HEAD',
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
def test_execute_no_upstream_raises(self):
"""A branch with no `origin/...` upstream cannot use `to_ref=None`."""
brave = self.repo.brave
self.repo.update_brave_version('1.0.0.0')
self.repo._run_git_command(['checkout', '-b', 'no-upstream'], brave)
self.repo.write_and_stage_file('x.txt', 'x', brave)
self.repo.commit('Add x', brave)
with self.assertRaises(brockit.InvalidInputException):
brockit.Rebase().execute(from_ref='HEAD~1',
to_ref=None,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
# The two defaulting tests below intercept the `git rebase` invocation so
# the rebase itself doesn't execute -- they verify only the *decision* of
# which `@` label is resolved into `from_ref`. Running the rebase here
# would inevitably conflict on the version-bump commit (`@previous`
# includes the most recent package.json change in the replay range, and
# that commit's diff can never apply cleanly onto a target with a
# different tag). In production this requires a prior `lift` to
# cherry-pick the matching bump onto the branch first.
def _intercept_rebase(self):
"""Returns a `patch` context that stubs only the `git rebase` call.
Other `terminal.run` invocations (used pervasively for ref resolution,
version reads, etc.) pass through unchanged.
"""
real_run = brockit.terminal.run
rebase_calls = []
def fake_run(cmd, *args, **kwargs):
if cmd[:2] == ['git', 'rebase']:
rebase_calls.append(cmd)
return None # `Rebase.execute` ignores the return value.
return real_run(cmd, *args, **kwargs)
ctx = patch.object(brockit.terminal, 'run', side_effect=fake_run)
return ctx, rebase_calls
@staticmethod
def _parse_rebase_cmd(cmd):
"""Extracts (to_ref, from_ref, branch) from a captured rebase cmd."""
return cmd[-3], cmd[-2], cmd[-1]
def test_execute_defaults_from_ref_to_previous_for_same_major(self):
"""When the target shares HEAD's major version, `from_ref=None`
resolves via `@previous` -- which is the parent of HEAD's most
recent package.json change."""
scenario = self._seed_rebase_scenario()
ctx, rebase_calls = self._intercept_rebase()
with ctx:
brockit.Rebase().execute(from_ref=None,
to_ref=scenario.v102,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
self.assertEqual(len(rebase_calls), 1)
to_ref, from_ref, branch = self._parse_rebase_cmd(rebase_calls[0])
# `_solve_brave_ref` returns a relative form like `<hash>~1`; resolve
# it back to a full hash for comparison.
resolved = self.repo._run_git_command(['rev-parse', from_ref],
self.repo.brave)
self.assertEqual(to_ref, scenario.v102)
self.assertEqual(resolved, scenario.v100)
self.assertEqual(branch, scenario.branch)
def test_execute_defaults_from_ref_to_previous_major_for_new_major(self):
"""When the target is on a different major version, `from_ref=None`
resolves via `@previous-major` -- which walks back over commits of
the same major and returns the parent of the major-bump commit."""
brave = self.repo.brave
self.repo.update_brave_version('1.0.0.0')
v200 = self.repo.update_brave_version('2.0.0.0')
# Feature branch off v200; the brave-only commit doesn't touch
# package.json, so HEAD's pjs is still at major 2.
self.repo._run_git_command(['checkout', '-b', 'feature-major'], brave)
self.repo.write_and_stage_file('feature.txt', 'feature\n', brave)
self.repo.commit('Add brave-only feature.txt', brave)
self.repo._run_git_command(
['push', '--set-upstream', 'origin', 'feature-major'], brave)
# Master advances to a new major.
self.repo._run_git_command(['checkout', 'master'], brave)
v300 = self.repo.update_brave_version('3.0.0.0')
self.repo._run_git_command(['checkout', 'feature-major'], brave)
# @previous-major walks v200 -> v100 (different major, break), and
# returns v200~1, which resolves to the v100 commit.
expected_resolved = self.repo._run_git_command(
['rev-parse', f'{v200}~1'], brave)
ctx, rebase_calls = self._intercept_rebase()
with ctx:
brockit.Rebase().execute(from_ref=None,
to_ref=v300,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
self.assertEqual(len(rebase_calls), 1)
to_ref, from_ref, branch = self._parse_rebase_cmd(rebase_calls[0])
resolved = self.repo._run_git_command(['rev-parse', from_ref], brave)
self.assertEqual(to_ref, v300)
self.assertEqual(resolved, expected_resolved)
self.assertEqual(branch, 'feature-major')
def test_execute_recommit_amends_first_rebased_commit(self):
"""`recommit=True` flips the first `pick` to `edit`, then amends and
continues. The rebased commit gets a fresh hash but keeps its
subject."""
scenario = self._seed_rebase_scenario()
before_head = self.repo._run_git_command(['rev-parse', 'HEAD'],
self.repo.brave)
brockit.Rebase().execute(from_ref=scenario.v101,
to_ref=scenario.v102,
recommit=True,
discard_regen_changes=False,
squash_minor_bumps=False)
after_head = self.repo._run_git_command(['rev-parse', 'HEAD'],
self.repo.brave)
self.assertNotEqual(after_head, before_head)
self.assertEqual(self._git_log_subjects()[-1],
'Add brave-only feature.txt')
parent = self.repo._run_git_command(['rev-parse', 'HEAD^'],
self.repo.brave)
self.assertEqual(parent, scenario.v102)
def test_execute_discard_regen_changes_strips_regen_commits(self):
"""`discard_regen_changes=True` removes `Update patches ...` and
`Updated strings ...` commits from the rebase plan, end-to-end
through the real `GIT_SEQUENCE_EDITOR` subprocess callback."""
scenario = self._seed_rebase_scenario()
# On top of the feature commit, add two regen-like commits and one
# genuine feature commit so we can prove only the regen ones are
# dropped.
self._commit_with_file(
'Update patches from Chromium 1.0.0.1 to Chromium 1.0.0.2.')
self._commit_with_file(
'Updated strings for Chromium 1.0.0.1 to Chromium 1.0.0.2.')
self._commit_with_file('[cr148] Another brave-only feature.')
brockit.Rebase().execute(from_ref=scenario.v101,
to_ref=scenario.v102,
recommit=False,
discard_regen_changes=True,
squash_minor_bumps=False)
subjects = self._git_log_subjects(scenario.v102 + '..HEAD')
self.assertEqual(subjects, [
'Add brave-only feature.txt',
'[cr148] Another brave-only feature.',
])
def test_execute_squash_minor_bumps_collapses_version_bumps(self):
"""`squash_minor_bumps=True` reorders the plan so version bumps come
first and squashes all-but-the-first; the final commit message is the
latest version-bump message."""
scenario = self._seed_rebase_scenario()
# Add an interleaved sequence: another version bump, a feature commit,
# then a third version bump.
v102_to_v103 = self._commit_with_file(
'Update from Chromium 1.0.0.2 to Chromium 1.0.0.3')
self._commit_with_file('[cr148] Some unrelated feature commit')
self._commit_with_file(
'Update from Chromium 1.0.0.3 to Chromium 1.0.0.4')
# Rebase from v101 (so all the commits above are in scope) onto v102.
brockit.Rebase().execute(from_ref=scenario.v101,
to_ref=scenario.v102,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=True)
subjects = self._git_log_subjects(scenario.v102 + '..HEAD')
# Version bumps collapse into the first one, taking the LAST message
# (per `fix_squash_commit_messages`). The unrelated feature commit
# and the original feature commit are kept as "others".
self.assertEqual(subjects, [
'Update from Chromium 1.0.0.3 to Chromium 1.0.0.4',
'Add brave-only feature.txt',
'[cr148] Some unrelated feature commit',
])
# Defensive: v102_to_v103 is gone (squashed into the merged bump).
all_hashes = self.repo._run_git_command(
['rev-list', scenario.v102 + '..HEAD'],
self.repo.brave).splitlines()
self.assertNotIn(v102_to_v103, all_hashes)
def test_execute_rebase_failure_raises_invalid_input(self):
"""A genuine git rebase failure (merge conflict on a tracked file) is
translated into `InvalidInputException`."""
brave = self.repo.brave
# Common ancestor: data.txt with content "A\n"
self.repo.write_and_stage_file('data.txt', 'A\n', brave)
base = self.repo.commit('Add data.txt', brave)
# Branch B: modifies data.txt to "B\n"
self.repo._run_git_command(['checkout', '-b', 'branch-b'], brave)
(brave / 'data.txt').write_text('B\n')
self.repo._run_git_command(['add', 'data.txt'], brave)
target = self.repo.commit('Set data.txt to B', brave)
# Branch A (current): modifies data.txt to "C\n"
self.repo._run_git_command(['checkout', '-b', 'branch-a', base], brave)
(brave / 'data.txt').write_text('C\n')
self.repo._run_git_command(['add', 'data.txt'], brave)
self.repo.commit('Set data.txt to C', brave)
with self.assertRaises(brockit.InvalidInputException):
brockit.Rebase().execute(from_ref=base,
to_ref=target,
recommit=False,
discard_regen_changes=False,
squash_minor_bumps=False)
if __name__ == '__main__':
unittest.main()