diff --git a/tools/cr/brockit.py b/tools/cr/brockit.py index c6a5a90645a..f81d6750230 100755 --- a/tools/cr/brockit.py +++ b/tools/cr/brockit.py @@ -179,6 +179,7 @@ import sys from typing import Optional, List, Dict from incendiary_error_handler import IncendiaryErrorHandler +from git_status import GitStatus from patchfile import Patchfile import repository from repository import Repository, CHROMIUM_SRC_PATH @@ -305,38 +306,6 @@ def _get_apply_patches_list(): return None -class GitStatus: - """Runs `git status` and provides a summary. - """ - - def __init__(self): - self.git_status = repository.brave.run_git('status', '--short') - - # a list of all deleted files, regardless of their staged status. - self.deleted = [] - - # a list of all modified files, regardless of their staged status. - self.modified = [] - - # a list of all untracked files. - self.untracked = [] - - for line in self.git_status.splitlines(): - [status, path] = line.lstrip().split(' ', 1) - if status == 'D': - self.deleted.append(path) - elif status == 'M': - self.modified.append(path) - elif status == '??': - self.untracked.append(path) - - def has_deleted_patch_files(self): - return any(path.endswith('.patch') for path in self.deleted) - - def has_untracked_patch_files(self): - return any(path.endswith('.patch') for path in self.untracked) - - @dataclass(frozen=True) class ApplyPatchesRecord: """A class to hold the continuation data for patches. diff --git a/tools/cr/git_status.py b/tools/cr/git_status.py new file mode 100644 index 00000000000..fbff40a3dd8 --- /dev/null +++ b/tools/cr/git_status.py @@ -0,0 +1,38 @@ +# Copyright (c) 2025 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/. + +import repository + + +class GitStatus: + """Runs `git status` and provides a summary. + """ + + def __init__(self): + self.git_status = repository.brave.run_git('status', '--short') + + # a list of all deleted files, regardless of their staged status. + self.deleted = [] + + # a list of all modified files, regardless of their staged status. + self.modified = [] + + # a list of all untracked files. + self.untracked = [] + + for line in self.git_status.splitlines(): + [status, path] = line.lstrip().split(' ', 1) + if status == 'D': + self.deleted.append(path) + elif status == 'M': + self.modified.append(path) + elif status == '??': + self.untracked.append(path) + + def has_deleted_patch_files(self): + return any(path.endswith('.patch') for path in self.deleted) + + def has_untracked_patch_files(self): + return any(path.endswith('.patch') for path in self.untracked) diff --git a/tools/cr/git_status_test.py b/tools/cr/git_status_test.py new file mode 100755 index 00000000000..899a1854ceb --- /dev/null +++ b/tools/cr/git_status_test.py @@ -0,0 +1,53 @@ +#!/usr/bin/env vpython3 +# Copyright (c) 2025 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/. + +import unittest +from pathlib import Path + +from test.fake_chromium_src import FakeChromiumSrc +from git_status import GitStatus + + +class GitStatusTest(unittest.TestCase): + + def setUp(self): + """Set up a fake Chromium repository for testing.""" + self.fake_chromium_src = FakeChromiumSrc() + self.fake_chromium_src.setup() + self.addCleanup(self.fake_chromium_src.cleanup) + + def test_has_deleted_patch_files(self): + """Test has_deleted_patch_files method.""" + self.assertFalse(GitStatus().has_deleted_patch_files()) + + # Create a patch file and commit it + patch_file = 'patches/test.patch' + self.fake_chromium_src.write_and_stage_file( + patch_file, 'Patch content', self.fake_chromium_src.brave) + self.fake_chromium_src.commit('Add test.patch', + self.fake_chromium_src.brave) + + # Delete the patch file + self.fake_chromium_src.delete_file(patch_file, + self.fake_chromium_src.brave) + + # Run GitStatus and verify the deleted patch file is detected + self.assertTrue(GitStatus().has_deleted_patch_files()) + + def test_has_untracked_patch_files(self): + """Test has_untracked_patch_files method.""" + self.assertFalse(GitStatus().has_untracked_patch_files()) + + # Create a patch file but do not stage it + Path(self.fake_chromium_src.brave / + 'test_untracked.patch').write_text('Untracked patch content') + + # Run GitStatus and verify the untracked patch file is detected + self.assertTrue(GitStatus().has_untracked_patch_files()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/cr/patchfile.py b/tools/cr/patchfile.py index ddd9645c712..b899cd9484b 100644 --- a/tools/cr/patchfile.py +++ b/tools/cr/patchfile.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field, replace from enum import Enum, auto import logging -from pathlib import PurePath +from pathlib import PurePath, Path import re import subprocess from typing import Optional, NamedTuple diff --git a/tools/cr/patchfile_test.py b/tools/cr/patchfile_test.py index 88211dd0b95..68e8344c9c9 100755 --- a/tools/cr/patchfile_test.py +++ b/tools/cr/patchfile_test.py @@ -311,6 +311,220 @@ class PatchfileTest(unittest.TestCase): 'patches/build-android-gyp-dex.py.patch')).path_from_repo(), 'brave/patches/build-android-gyp-dex.py.patch') + def test_fetch_source_from_git(self): + """Test fetch_source_from_git with renamed patch file.""" + test_idl = Path('chrome/common/extensions/api/developer_private.idl') + self.fake_chromium_src.write_and_stage_file( + test_idl, """ + enum ExtensionType { + HOSTED_APP, + PLATFORM_APP, + LEGACY_PACKAGED_APP, + EXTENSION, + THEME, + SHARED_MODULE + }; + """, self.fake_chromium_src.chromium) + + self.fake_chromium_src.commit('Add developer_private.idl', + self.fake_chromium_src.chromium) + + # Create a patch for the file + target_file = self.fake_chromium_src.chromium / test_idl + target_file.write_text(target_file.read_text().replace( + 'HOSTED_APP', 'HOSTED_APP,\n NEW_TYPE')) + self.fake_chromium_src.run_update_patches() + + # Rename the patch file + original_patch_path = ( + self.fake_chromium_src.get_patchfile_path_for_source( + self.fake_chromium_src.chromium, test_idl)) + renamed_patch_path = original_patch_path.with_name( + 'renamed_developer_private.idl.patch') + (self.fake_chromium_src.brave / original_patch_path).rename( + self.fake_chromium_src.brave / renamed_patch_path) + + # Fetch the source from the renamed patch file + patchfile = Patchfile(path=renamed_patch_path) + + # Verify the source file path matches the original file + self.assertEqual( + str(patchfile.fetch_source_from_git().source_from_git), + str(test_idl)) + + def test_get_last_commit_for_source(self): + """Test get_last_commit_for_source method.""" + test_file = Path('chrome/common/extensions/api/developer_private.idl') + self.fake_chromium_src.write_and_stage_file( + test_file, """ + enum ExtensionType { + HOSTED_APP, + PLATFORM_APP, + LEGACY_PACKAGED_APP, + EXTENSION, + THEME, + SHARED_MODULE + }; + """, self.fake_chromium_src.chromium) + + # Commit the file and get the short commit hash + initial_commit_hash = self.fake_chromium_src.commit( + 'Add developer_private.idl', self.fake_chromium_src.chromium)[:7] + + # Create a patch for the file + target_file = self.fake_chromium_src.chromium / test_file + target_file.write_text(target_file.read_text().replace( + 'HOSTED_APP', 'HOSTED_APP,\n NEW_TYPE')) + self.fake_chromium_src.run_update_patches() + + # Add a few empty commits + self.fake_chromium_src.commit_empty('Empty commit 1', + self.fake_chromium_src.chromium) + self.fake_chromium_src.commit_empty('Empty commit 2', + self.fake_chromium_src.chromium) + + # Verify the last commit for the source matches the initial commit hash + patchfile_path = self.fake_chromium_src.get_patchfile_path_for_source( + self.fake_chromium_src.chromium, test_file) + patchfile = Patchfile(path=patchfile_path) + self.assertEqual(patchfile.get_last_commit_for_source(), + initial_commit_hash) + + # Delete the file and commit + self.fake_chromium_src.delete_file(test_file, + self.fake_chromium_src.chromium) + delete_commit_hash = self.fake_chromium_src.commit( + 'Delete developer_private.idl', + self.fake_chromium_src.chromium)[:7] + + # Add a few more empty commits + self.fake_chromium_src.commit_empty('Empty commit 3', + self.fake_chromium_src.chromium) + self.fake_chromium_src.commit_empty('Empty commit 4', + self.fake_chromium_src.chromium) + + # Get the patch file path + patchfile = Patchfile(path=patchfile_path) + + # Verify the last commit for the source matches the delete commit hash + self.assertEqual(patchfile.get_last_commit_for_source(), + delete_commit_hash) + + def test_get_source_removal_status(self): + """Test get_source_removal_status for a deleted source file.""" + test_file = Path('chrome/common/extensions/api/developer_private.idl') + self.fake_chromium_src.write_and_stage_file( + test_file, """ + enum ExtensionType { + HOSTED_APP, + PLATFORM_APP, + LEGACY_PACKAGED_APP, + EXTENSION, + THEME, + SHARED_MODULE + }; + """, self.fake_chromium_src.chromium) + + # Commit the file + self.fake_chromium_src.commit('Add developer_private.idl', + self.fake_chromium_src.chromium) + + # Add a few empty commits + self.fake_chromium_src.commit_empty('Empty commit 1', + self.fake_chromium_src.chromium) + self.fake_chromium_src.commit_empty('Empty commit 2', + self.fake_chromium_src.chromium) + + # Create a patch for the file + target_file = self.fake_chromium_src.chromium / test_file + target_file.write_text(target_file.read_text().replace( + 'HOSTED_APP', 'HOSTED_APP,\n NEW_TYPE')) + self.fake_chromium_src.run_update_patches() + + # Delete the file and commit + self.fake_chromium_src.delete_file(test_file, + self.fake_chromium_src.chromium) + delete_commit_hash = self.fake_chromium_src.commit( + 'Delete developer_private.idl', self.fake_chromium_src.chromium) + + # Get the patch file path + patchfile_path = self.fake_chromium_src.get_patchfile_path_for_source( + self.fake_chromium_src.chromium, test_file) + patchfile = Patchfile(path=patchfile_path) + + # Add a few more empty commits + self.fake_chromium_src.commit_empty('Empty commit 3', + self.fake_chromium_src.chromium) + self.fake_chromium_src.commit_empty('Empty commit 4', + self.fake_chromium_src.chromium) + + # Verify the source removal status + removal_status = patchfile.get_source_removal_status( + delete_commit_hash) + self.assertEqual(removal_status.status, 'D') # 'D' indicates deletion + self.assertIn('Delete developer_private.idl', + removal_status.commit_details) + self.assertIsNone(removal_status.renamed_to) + + def test_get_source_rename_status(self): + """Test get_source_removal_status for a renamed source file.""" + test_file = Path('chrome/common/extensions/api/developer_private.idl') + renamed_file = Path('chrome/common/extensions/api/renamed_private.idl') + + # Write and commit the original file + self.fake_chromium_src.write_and_stage_file( + test_file, """ + enum ExtensionType { + HOSTED_APP, + PLATFORM_APP, + LEGACY_PACKAGED_APP, + EXTENSION, + THEME, + SHARED_MODULE + }; + """, self.fake_chromium_src.chromium) + self.fake_chromium_src.commit('Add developer_private.idl', + self.fake_chromium_src.chromium) + + # Add a few empty commits + self.fake_chromium_src.commit_empty('Empty commit 1', + self.fake_chromium_src.chromium) + self.fake_chromium_src.commit_empty('Empty commit 2', + self.fake_chromium_src.chromium) + + # Create a patch for the file + target_file = self.fake_chromium_src.chromium / test_file + target_file.write_text(target_file.read_text().replace( + 'HOSTED_APP', 'HOSTED_APP,\n NEW_TYPE')) + self.fake_chromium_src.run_update_patches() + + # Rename the file and commit + (self.fake_chromium_src.chromium / test_file).rename( + self.fake_chromium_src.chromium / renamed_file) + self.fake_chromium_src._run_git_command( + ['add', '-A'], self.fake_chromium_src.chromium) + rename_commit_hash = self.fake_chromium_src.commit( + 'Rename developer_private.idl to renamed_private.idl', + self.fake_chromium_src.chromium) + + # Get the patch file path + patchfile_path = self.fake_chromium_src.get_patchfile_path_for_source( + self.fake_chromium_src.chromium, test_file) + patchfile = Patchfile(path=patchfile_path) + + # Add a few more empty commits + self.fake_chromium_src.commit_empty('Empty commit 3', + self.fake_chromium_src.chromium) + self.fake_chromium_src.commit_empty('Empty commit 4', + self.fake_chromium_src.chromium) + + # Verify the source rename status + rename_status = patchfile.get_source_removal_status(rename_commit_hash) + self.assertEqual(rename_status.status, 'R') # 'R' indicates rename + self.assertIn('Rename developer_private.idl to renamed_private.idl', + rename_status.commit_details) + self.assertEqual(rename_status.renamed_to, str(renamed_file)) + if __name__ == "__main__": unittest.main()