diff --git a/packages/browseros/build/cli/dev.py b/packages/browseros/build/cli/dev.py index 5fc5dc1f0..983a61f41 100755 --- a/packages/browseros/build/cli/dev.py +++ b/packages/browseros/build/cli/dev.py @@ -170,6 +170,9 @@ def extract_commit( base: Optional[str] = Option( None, "--base", help="Extract full diff from base commit for files in COMMIT" ), + feature: bool = Option( + False, "--feature", help="Add extracted files to a feature in features.yaml" + ), ): """Extract patches from a single commit""" ctx = create_build_context(state.chromium_src) @@ -190,6 +193,7 @@ def extract_commit( force=force, include_binary=include_binary, base=base, + feature=feature, ) except Exception as e: log_error(f"Failed to extract commit: {e}") @@ -201,6 +205,9 @@ def extract_patch_cmd( chromium_path: str = Argument(..., help="Chromium file path (e.g., chrome/common/foo.h)"), base: str = Option(..., "--base", "-b", help="Base commit to diff against"), force: bool = Option(False, "--force", "-f", help="Overwrite existing patch without prompting"), + feature: bool = Option( + False, "--feature", help="Add extracted file to a feature in features.yaml" + ), ): """Extract patch for a specific file""" ctx = create_build_context(state.chromium_src) @@ -215,6 +222,17 @@ def extract_patch_cmd( raise typer.Exit(1) log_success(f"Successfully extracted patch for: {chromium_path}") + # Handle --feature flag + if feature: + from ..modules.feature import prompt_feature_selection, add_files_to_feature + + result = prompt_feature_selection(ctx, base[:12], None) + if result is None: + log_warning("Skipped adding file to feature") + else: + feature_name, description = result + add_files_to_feature(ctx, feature_name, description, [chromium_path]) + @extract_app.command(name="range") def extract_range( @@ -232,6 +250,9 @@ def extract_range( "--base", help="Use different base for diff (full diff from base for files in range)", ), + feature: bool = Option( + False, "--feature", help="Add extracted files to a feature in features.yaml" + ), ): """Extract patches from a range of commits""" ctx = create_build_context(state.chromium_src) @@ -254,6 +275,7 @@ def extract_range( include_binary=include_binary, squash=squash, base=base, + feature=feature, ) except Exception as e: log_error(f"Failed to extract range: {e}") @@ -266,10 +288,12 @@ def apply_all( interactive: bool = Option( True, "--interactive/--no-interactive", "-i/-n", help="Interactive mode" ), - commit: bool = Option(False, "--commit", "-c", help="Commit after each patch"), reset_to: Optional[str] = Option( None, "--reset-to", "-r", help="Reset files to this commit before applying patches" ), + annotate: bool = Option( + False, "--annotate", "-a", help="Create git commits per feature after applying" + ), ): """Apply all patches from chromium_patches/""" ctx = create_build_context(state.chromium_src) @@ -281,7 +305,7 @@ def apply_all( module = ApplyAllModule() try: module.validate(ctx) - module.execute(ctx, interactive=interactive, commit=commit, reset_to=reset_to) + module.execute(ctx, interactive=interactive, reset_to=reset_to, annotate=annotate) except Exception as e: log_error(f"Failed to apply patches: {e}") raise typer.Exit(1) @@ -293,10 +317,12 @@ def apply_feature( interactive: bool = Option( True, "--interactive/--no-interactive", "-i/-n", help="Interactive mode" ), - commit: bool = Option(False, "--commit", "-c", help="Commit after applying"), reset_to: Optional[str] = Option( None, "--reset-to", "-r", help="Reset files to this commit before applying patches" ), + annotate: bool = Option( + False, "--annotate", "-a", help="Create git commit for this feature after applying" + ), ): """Apply patches for a specific feature""" ctx = create_build_context(state.chromium_src) @@ -309,7 +335,7 @@ def apply_feature( try: module.validate(ctx) module.execute( - ctx, feature_name=feature_name, interactive=interactive, commit=commit, reset_to=reset_to + ctx, feature_name=feature_name, interactive=interactive, reset_to=reset_to, annotate=annotate ) except Exception as e: log_error(f"Failed to apply feature: {e}") @@ -403,5 +429,61 @@ def feature_add( raise typer.Exit(1) +@feature_app.command(name="classify") +def feature_classify(): + """Classify unclassified patch files into features + + Lists all patches in chromium_patches/ that are not in any feature, + then prompts one-by-one to assign each to a feature. + + Examples: + browseros dev feature classify + """ + ctx = create_build_context(state.chromium_src) + if not ctx: + raise typer.Exit(1) + + from ..modules.feature import ClassifyFeaturesModule + + module = ClassifyFeaturesModule() + try: + module.validate(ctx) + module.execute(ctx) + except Exception as e: + log_error(f"Failed to classify features: {e}") + raise typer.Exit(1) + + +# Annotate command +@app.command(name="annotate") +def annotate_cmd( + feature_name: Optional[str] = Argument( + None, help="Optional: specific feature to annotate (default: all features)" + ), +): + """Create git commits organized by features from features.yaml + + For each feature with modified files, creates a commit with the format: + "{feature_name}: {description}" + + Examples: + browseros dev annotate -S /path/to/chromium + browseros dev annotate llm-chat -S /path/to/chromium + """ + ctx = create_build_context(state.chromium_src) + if not ctx: + raise typer.Exit(1) + + from ..modules.annotate import AnnotateModule + + module = AnnotateModule() + try: + module.validate(ctx) + module.execute(ctx, feature_name=feature_name) + except Exception as e: + log_error(f"Failed to annotate: {e}") + raise typer.Exit(1) + + if __name__ == "__main__": app() diff --git a/packages/browseros/build/features.yaml b/packages/browseros/build/features.yaml index 364e3960e..2116bc493 100644 --- a/packages/browseros/build/features.yaml +++ b/packages/browseros/build/features.yaml @@ -1,358 +1,370 @@ -version: "1.0" +version: '1.0' features: add-sparkle-info-plist-keys: - description: "patch: app-info.plist changes" + description: 'patch: app-info.plist changes' files: - - chrome/app/app-Info.plist + - chrome/app/app-Info.plist adding-new-vector-icons: - description: "patch: adding-new-vector-icons" + description: 'patch: adding-new-vector-icons' files: - - components/vector_icons/BUILD.gn - - components/vector_icons/chat_orange.icon - - components/vector_icons/clash_of_gpts.icon + - components/vector_icons/BUILD.gn + - components/vector_icons/chat_orange.icon + - components/vector_icons/clash_of_gpts.icon branding-file-updates: description: browseros branding for file paths files: - - chrome/common/chrome_constants.cc - - chrome/common/chrome_paths_linux.cc - - chrome/install_static/chromium_install_modes.cc - - chrome/install_static/chromium_install_modes.h - - components/os_crypt/sync/keychain_password_mac.mm + - chrome/common/chrome_constants.cc + - chrome/common/chrome_paths_linux.cc + - chrome/install_static/chromium_install_modes.cc + - chrome/install_static/chromium_install_modes.h + - components/os_crypt/sync/keychain_password_mac.mm branding-resources: description: browseros branding resources and assets files: - - chrome/app/chromium_strings.grd - - chrome/app/settings_chromium_strings.grdp - - chrome/app/theme/chromium/BRANDING - - chrome/app/theme/chromium/chromeos/ - - chrome/app/theme/chromium/chromium.ai - - chrome/app/theme/chromium/linux/ - - chrome/app/theme/chromium/mac/ - - chrome/app/theme/chromium/product_logo.ai - - chrome/app/theme/chromium/product_logo.svg - - chrome/app/theme/chromium/product_logo.png - - chrome/app/theme/chromium/product_logo_16.png - - chrome/app/theme/chromium/product_logo_22.png - - chrome/app/theme/chromium/product_logo_22_mono.png - - chrome/app/theme/chromium/product_logo_24.png - - chrome/app/theme/chromium/product_logo_32.png - - chrome/app/theme/chromium/product_logo_48.png - - chrome/app/theme/chromium/product_logo_64.png - - chrome/app/theme/chromium/product_logo_128.png - - chrome/app/theme/chromium/product_logo_192.png - - chrome/app/theme/chromium/product_logo_256.png - - chrome/app/theme/chromium/product_logo_animation.svg - - chrome/app/theme/chromium/product_logo_name_22.png - - chrome/app/theme/chromium/product_logo_name_22_2x.png - - chrome/app/theme/chromium/product_logo_name_22_white.png - - chrome/app/theme/chromium/product_logo_name_22_white_2x.png - - chrome/app/theme/chromium/win/ - - chrome/app/theme/default_100_percent/chromium/ - - chrome/app/theme/default_200_percent/chromium/ - - chrome/enterprise_companion/branding.gni + - chrome/app/chromium_strings.grd + - chrome/app/settings_chromium_strings.grdp + - chrome/app/theme/ + - chrome/enterprise_companion/branding.gni ai-settings-page: description: llm settings page files: - - chrome/browser/extensions/api/settings_private/prefs_util.cc - - chrome/browser/prefs/browser_prefs.cc - - chrome/browser/prefs/browser_prefs.h - - chrome/browser/resources/settings/BUILD.gn - - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html - - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts - - chrome/browser/resources/settings/route.ts - - chrome/browser/resources/settings/router.ts - - chrome/browser/resources/settings/settings.ts - - chrome/browser/resources/settings/settings_main/settings_main.html - - chrome/browser/resources/settings/settings_main/settings_main.ts - - chrome/browser/resources/settings/settings_menu/settings_menu.html - - chrome/common/pref_names.h + - chrome/browser/extensions/api/settings_private/prefs_util.cc + - chrome/browser/prefs/browser_prefs.cc + - chrome/browser/prefs/browser_prefs.h + - chrome/browser/resources/settings/BUILD.gn + - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html + - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts + - chrome/browser/resources/settings/route.ts + - chrome/browser/resources/settings/router.ts + - chrome/browser/resources/settings/settings.ts + - chrome/browser/resources/settings/settings_main/settings_main.html + - chrome/browser/resources/settings/settings_main/settings_main.ts + - chrome/browser/resources/settings/settings_menu/settings_menu.html + - chrome/common/pref_names.h api: description: browseros API files: - - chrome/browser/extensions/BUILD.gn - - chrome/browser/extensions/api/browser_os/browser_os_api.cc - - chrome/browser/extensions/api/browser_os/browser_os_api.h - - chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc - - chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h - - chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc - - chrome/browser/extensions/api/browser_os/browser_os_api_utils.h - - chrome/browser/extensions/api/browser_os/browser_os_change_detector.cc - - chrome/browser/extensions/api/browser_os/browser_os_change_detector.h - - chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc - - chrome/browser/extensions/api/browser_os/browser_os_content_processor.h - - chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc - - chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h - - chrome/browser/extensions/chrome_extensions_browser_api_provider.cc - - chrome/browser/media/extension_media_access_handler.cc - - chrome/common/extensions/api/_api_features.json - - chrome/common/extensions/api/_permission_features.json - - chrome/common/extensions/api/api_sources.gni - - chrome/common/extensions/api/browser_os.idl - - chrome/common/extensions/permissions/chrome_api_permissions.cc - - extensions/browser/extension_function_histogram_value.h - - extensions/common/mojom/api_permission_id.mojom - - tools/metrics/histograms/metadata/extensions/enums.xml + - chrome/browser/extensions/BUILD.gn + - chrome/browser/extensions/api/browser_os/browser_os_api.cc + - chrome/browser/extensions/api/browser_os/browser_os_api.h + - chrome/browser/extensions/api/browser_os/browser_os_api_helpers.cc + - chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h + - chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc + - chrome/browser/extensions/api/browser_os/browser_os_api_utils.h + - chrome/browser/extensions/api/browser_os/browser_os_change_detector.cc + - chrome/browser/extensions/api/browser_os/browser_os_change_detector.h + - chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc + - chrome/browser/extensions/api/browser_os/browser_os_content_processor.h + - chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc + - chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h + - chrome/browser/extensions/api/side_panel/side_panel_api.h + - chrome/browser/extensions/api/side_panel/side_panel_service.cc + - chrome/browser/extensions/api/side_panel/side_panel_service.h + - chrome/browser/extensions/chrome_extensions_browser_api_provider.cc + - chrome/browser/media/extension_media_access_handler.cc + - chrome/browser/ui/extensions/extension_side_panel_utils.h + - chrome/browser/ui/views/side_panel/extensions/extension_side_panel_utils.cc + - chrome/common/extensions/api/_api_features.json + - chrome/common/extensions/api/_permission_features.json + - chrome/common/extensions/api/api_sources.gni + - chrome/common/extensions/api/browser_os.idl + - chrome/common/extensions/api/side_panel.idl + - chrome/common/extensions/permissions/chrome_api_permissions.cc + - extensions/browser/extension_function_histogram_value.h + - extensions/common/mojom/api_permission_id.mojom + - tools/metrics/histograms/metadata/extensions/enums.xml server: description: browseros server files: - - chrome/browser/browseros_server/ - - base/threading/thread_restrictions.h + - base/threading/thread_restrictions.h + - chrome/browser/browseros_server/ + - chrome/browser/browseros_server/BUILD.gn + - chrome/browser/browseros_server/browseros_server_manager.cc + - chrome/browser/browseros_server/browseros_server_manager.h + - chrome/browser/browseros_server/browseros_server_prefs.cc + - chrome/browser/browseros_server/browseros_server_prefs.h + - chrome/browser/browseros_server/validate_resources.py metrics: description: browseros metrics files: - - chrome/browser/metrics/chrome_metrics_service_client.cc - - chrome/browser/prefs/browser_prefs.cc - - chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc - - chrome/browser/ui/BUILD.gn - - chrome/browser/ui/webui/settings/browseros_metrics_handler.cc - - chrome/browser/ui/webui/settings/browseros_metrics_handler.h - - chrome/browser/ui/webui/settings/settings_ui.cc - - chrome/common/pref_names.h - - components/metrics/browseros_metrics/BUILD.gn - - components/metrics/browseros_metrics/DEPS - - components/metrics/browseros_metrics/browseros_metrics.cc - - components/metrics/browseros_metrics/browseros_metrics.h - - components/metrics/browseros_metrics/browseros_metrics_prefs.cc - - components/metrics/browseros_metrics/browseros_metrics_prefs.h - - components/metrics/browseros_metrics/browseros_metrics_service.cc - - components/metrics/browseros_metrics/browseros_metrics_service.h - - components/metrics/browseros_metrics/browseros_metrics_service_factory.cc - - components/metrics/browseros_metrics/browseros_metrics_service_factory.h + - chrome/browser/metrics/chrome_metrics_service_client.cc + - chrome/browser/prefs/browser_prefs.cc + - chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc + - chrome/browser/ui/BUILD.gn + - chrome/browser/ui/webui/settings/browseros_metrics_handler.cc + - chrome/browser/ui/webui/settings/browseros_metrics_handler.h + - chrome/browser/ui/webui/settings/settings_ui.cc + - chrome/common/pref_names.h + - components/metrics/browseros_metrics/BUILD.gn + - components/metrics/browseros_metrics/DEPS + - components/metrics/browseros_metrics/browseros_metrics.cc + - components/metrics/browseros_metrics/browseros_metrics.h + - components/metrics/browseros_metrics/browseros_metrics_prefs.cc + - components/metrics/browseros_metrics/browseros_metrics_prefs.h + - components/metrics/browseros_metrics/browseros_metrics_service.cc + - components/metrics/browseros_metrics/browseros_metrics_service.h + - components/metrics/browseros_metrics/browseros_metrics_service_factory.cc + - components/metrics/browseros_metrics/browseros_metrics_service_factory.h ota-updater: description: extensions ota updater files: - - chrome/browser/extensions/BUILD.gn - - chrome/browser/extensions/api/developer_private/extension_info_generator_shared.cc - - chrome/browser/extensions/browseros_extension_constants.h - - chrome/browser/extensions/browseros_external_loader.cc - - chrome/browser/extensions/browseros_external_loader.h - - chrome/browser/extensions/chrome_extension_registrar_delegate.cc - - chrome/browser/extensions/extension_web_ui_override_registrar.cc - - chrome/browser/extensions/external_provider_impl.cc - - chrome/browser/ui/extensions/settings_overridden_params_providers.cc + - chrome/browser/extensions/BUILD.gn + - chrome/browser/extensions/api/developer_private/extension_info_generator_shared.cc + - chrome/browser/extensions/browseros_extension_constants.h + - chrome/browser/extensions/browseros_external_loader.cc + - chrome/browser/extensions/browseros_external_loader.h + - chrome/browser/extensions/chrome_extension_registrar_delegate.cc + - chrome/browser/extensions/extension_web_ui_override_registrar.cc + - chrome/browser/extensions/external_provider_impl.cc + - chrome/browser/ui/extensions/settings_overridden_params_providers.cc chrome-importer: description: chrome importer files: - - chrome/app/generated_resources.grd - - chrome/app/settings_strings.grdp - - chrome/browser/extensions/api/settings_private/prefs_util.cc - - chrome/browser/importer/external_process_importer_client.cc - - chrome/browser/importer/external_process_importer_client.h - - chrome/browser/importer/importer_list.cc - - chrome/browser/importer/importer_uma.cc - - chrome/browser/importer/in_process_importer_bridge.cc - - chrome/browser/importer/in_process_importer_bridge.h - - chrome/browser/importer/profile_writer.cc - - chrome/browser/importer/profile_writer.h - - chrome/browser/resources/settings/people_page/import_data_browser_proxy.ts - - chrome/browser/resources/settings/people_page/import_data_dialog.html - - chrome/browser/ui/webui/settings/import_data_handler.cc - - chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc - - chrome/browser/ui/webui/settings/settings_ui.cc - - chrome/common/importer/importer_bridge.h - - chrome/common/importer/importer_data_types.h - - chrome/common/importer/importer_type.h - - chrome/common/importer/profile_import.mojom - - chrome/common/importer/profile_import_process_param_traits_macros.h - - chrome/common/pref_names.h - - chrome/utility/BUILD.gn - - chrome/utility/importer/chrome_importer.cc - - chrome/utility/importer/chrome_importer.h - - chrome/utility/importer/external_process_importer_bridge.cc - - chrome/utility/importer/external_process_importer_bridge.h - - chrome/utility/importer/importer_creator.cc - - tools/metrics/histograms/metadata/sql/histograms.xml + - chrome/app/generated_resources.grd + - chrome/app/settings_strings.grdp + - chrome/browser/extensions/api/settings_private/prefs_util.cc + - chrome/browser/importer/external_process_importer_client.cc + - chrome/browser/importer/external_process_importer_client.h + - chrome/browser/importer/importer_list.cc + - chrome/browser/importer/importer_uma.cc + - chrome/browser/importer/in_process_importer_bridge.cc + - chrome/browser/importer/in_process_importer_bridge.h + - chrome/browser/importer/profile_writer.cc + - chrome/browser/importer/profile_writer.h + - chrome/browser/resources/settings/people_page/import_data_browser_proxy.ts + - chrome/browser/resources/settings/people_page/import_data_dialog.html + - chrome/browser/ui/webui/settings/import_data_handler.cc + - chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc + - chrome/browser/ui/webui/settings/settings_ui.cc + - chrome/common/importer/importer_bridge.h + - chrome/common/importer/importer_data_types.h + - chrome/common/importer/importer_type.h + - chrome/common/importer/profile_import.mojom + - chrome/common/importer/profile_import_process_param_traits_macros.h + - chrome/common/pref_names.h + - chrome/utility/BUILD.gn + - chrome/utility/importer/chrome_importer.cc + - chrome/utility/importer/chrome_importer.h + - chrome/utility/importer/external_process_importer_bridge.cc + - chrome/utility/importer/external_process_importer_bridge.h + - chrome/utility/importer/importer_creator.cc + - components/user_data_importer/common/importer_data_types.h + - components/user_data_importer/common/importer_type.h + - tools/metrics/histograms/metadata/sql/histograms.xml chrome-version-updater: - description: "patch: chrome version update" + description: 'patch: chrome version update' files: - - chrome/VERSION + - chrome/VERSION default-light-mode: description: enable light mode as default theme files: - - chrome/browser/themes/theme_service_factory.cc + - chrome/browser/themes/theme_service_factory.cc disable-chrome-labs-pinning: - description: "patch: disable-chrome-labs-pinning" + description: 'patch: disable-chrome-labs-pinning' files: - - chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.cc - - chrome/browser/ui/toolbar/toolbar_pref_names.cc + - chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.cc + - chrome/browser/ui/toolbar/toolbar_pref_names.cc disable-google-key-info-bar: - description: "patch: disable-google-key-info-bar" + description: 'patch: disable-google-key-info-bar' files: - - chrome/browser/ui/startup/google_api_keys_infobar_delegate.cc + - chrome/browser/ui/startup/google_api_keys_infobar_delegate.cc disable-info-bar-in-cdp: - description: "patch: disable-info-bar-in-cdp" + description: 'patch: disable-info-bar-in-cdp' files: - - chrome/browser/extensions/api/debugger/debugger_api.cc + - chrome/browser/extensions/api/debugger/debugger_api.cc disable-sidepanel-animation: description: disable sidepanel animation files: - - chrome/browser/ui/views/side_panel/side_panel.cc - - chrome/browser/ui/views/side_panel/side_panel.h + - chrome/browser/ui/views/side_panel/side_panel.cc + - chrome/browser/ui/views/side_panel/side_panel.h disable-user-gesture-restriction-on-sidepanel: - description: "patch: disable-user-gesture-restriction-on-sidepanel" + description: 'patch: disable-user-gesture-restriction-on-sidepanel' files: - - chrome/browser/extensions/api/side_panel/side_panel_api.cc + - chrome/browser/extensions/api/side_panel/side_panel_api.cc first-run: description: first run files: - - chrome/browser/chrome_browser_main.cc - - chrome/browser/ui/webui/chrome_web_ui_configs.cc - - chrome/browser/ui/webui/nxtscape_first_run.h - - chrome/common/webui_url_constants.cc + - chrome/browser/chrome_browser_main.cc + - chrome/browser/ui/webui/chrome_web_ui_configs.cc + - chrome/browser/ui/webui/nxtscape_first_run.h + - chrome/common/webui_url_constants.cc llm-chat: description: llm chat and updates files: - - chrome/app/chrome_command_ids.h - - chrome/app/generated_resources.grd - - chrome/browser/global_keyboard_shortcuts_mac.mm - - chrome/browser/ui/actions/chrome_action_id.h - - chrome/browser/ui/browser_actions.cc - - chrome/browser/ui/browser_command_controller.cc - - chrome/browser/ui/toolbar/toolbar_pref_names.cc - - chrome/browser/ui/ui_features.cc - - chrome/browser/ui/ui_features.h - - chrome/browser/ui/views/accelerator_table.cc - - chrome/browser/ui/views/side_panel/BUILD.gn - - chrome/browser/ui/views/side_panel/browseros_simple_page_extractor.cc - - chrome/browser/ui/views/side_panel/browseros_simple_page_extractor.h - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_coordinator.cc - - chrome/browser/ui/views/side_panel/side_panel_entry_id.h - - chrome/browser/ui/views/side_panel/side_panel_prefs.cc - - chrome/browser/ui/views/side_panel/side_panel_util.cc - - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.cc - - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h - - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.cc - - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h - - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom - - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc + - chrome/app/chrome_command_ids.h + - chrome/app/generated_resources.grd + - chrome/browser/global_keyboard_shortcuts_mac.mm + - chrome/browser/ui/actions/chrome_action_id.h + - chrome/browser/ui/browser_actions.cc + - chrome/browser/ui/browser_command_controller.cc + - chrome/browser/ui/toolbar/toolbar_pref_names.cc + - chrome/browser/ui/ui_features.cc + - chrome/browser/ui/ui_features.h + - chrome/browser/ui/views/accelerator_table.cc + - chrome/browser/ui/views/side_panel/BUILD.gn + - chrome/browser/ui/views/side_panel/browseros_simple_page_extractor.cc + - chrome/browser/ui/views/side_panel/browseros_simple_page_extractor.h + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_coordinator.cc + - chrome/browser/ui/views/side_panel/side_panel_entry_id.h + - chrome/browser/ui/views/side_panel/side_panel_prefs.cc + - chrome/browser/ui/views/side_panel/side_panel_util.cc + - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.cc + - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_panel_coordinator.h + - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.cc + - chrome/browser/ui/views/side_panel/third_party_llm/third_party_llm_view.h + - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom + - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc llm-hub: description: llm-hub files: - - chrome/app/chrome_command_ids.h - - chrome/app/generated_resources.grd - - chrome/browser/global_keyboard_shortcuts_mac.mm - - chrome/browser/ui/actions/chrome_action_id.h - - chrome/browser/ui/browser_actions.cc - - chrome/browser/ui/browser_command_controller.cc - - chrome/browser/ui/toolbar/toolbar_pref_names.cc - - chrome/browser/ui/ui_features.cc - - chrome/browser/ui/ui_features.h - - chrome/browser/ui/views/accelerator_table.cc - - chrome/browser/ui/views/side_panel/BUILD.gn - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_coordinator.cc - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_coordinator.h - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_view.cc - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_view.h - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_window.cc - - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_window.h - - chrome/browser/ui/views/side_panel/side_panel_entry_id.h - - chrome/browser/ui/views/side_panel/side_panel_prefs.cc - - chrome/browser/ui/views/side_panel/side_panel_util.cc - - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.cc - - chrome/browser/ui/webui/BUILD.gn - - chrome/browser/ui/webui/chrome_web_ui_configs.cc - - chrome/browser/ui/webui/clash_of_gpts/clash_of_gpts_ui.cc - - chrome/browser/ui/webui/clash_of_gpts/clash_of_gpts_ui.h - - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom - - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc - - chrome/common/webui_url_constants.h + - chrome/app/chrome_command_ids.h + - chrome/app/generated_resources.grd + - chrome/browser/global_keyboard_shortcuts_mac.mm + - chrome/browser/ui/actions/chrome_action_id.h + - chrome/browser/ui/browser_actions.cc + - chrome/browser/ui/browser_command_controller.cc + - chrome/browser/ui/toolbar/toolbar_pref_names.cc + - chrome/browser/ui/ui_features.cc + - chrome/browser/ui/ui_features.h + - chrome/browser/ui/views/accelerator_table.cc + - chrome/browser/ui/views/side_panel/BUILD.gn + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_coordinator.cc + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_coordinator.h + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_view.cc + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_view.h + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_window.cc + - chrome/browser/ui/views/side_panel/clash_of_gpts/clash_of_gpts_window.h + - chrome/browser/ui/views/side_panel/side_panel_entry_id.h + - chrome/browser/ui/views/side_panel/side_panel_prefs.cc + - chrome/browser/ui/views/side_panel/side_panel_util.cc + - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.cc + - chrome/browser/ui/webui/BUILD.gn + - chrome/browser/ui/webui/chrome_web_ui_configs.cc + - chrome/browser/ui/webui/clash_of_gpts/clash_of_gpts_ui.cc + - chrome/browser/ui/webui/clash_of_gpts/clash_of_gpts_ui.h + - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar.mojom + - chrome/browser/ui/webui/side_panel/customize_chrome/customize_toolbar/customize_toolbar_handler.cc + - chrome/common/webui_url_constants.h llm-settings-page-updates: - description: "llm settings page: updates" + description: 'llm settings page: updates' files: - - chrome/browser/resources/settings/BUILD.gn - - chrome/browser/resources/settings/nxtscape_page/models_data.html - - chrome/browser/resources/settings/nxtscape_page/models_data.ts - - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html - - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts + - chrome/browser/resources/settings/BUILD.gn + - chrome/browser/resources/settings/nxtscape_page/models_data.html + - chrome/browser/resources/settings/nxtscape_page/models_data.ts + - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html + - chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts mac-sparkle-updater: - description: "patch: nxtscape-updater-sparkle" + description: 'patch: nxtscape-updater-sparkle' files: - - chrome/BUILD.gn - - chrome/browser/BUILD.gn - - chrome/browser/mac/chrome_browser_main_extra_parts_mac.h - - chrome/browser/mac/chrome_browser_main_extra_parts_mac.mm - - chrome/browser/mac/sparkle_glue.h - - chrome/browser/mac/sparkle_glue.mm - - chrome/browser/mac/su_updater.h - - chrome/browser/sparkle_buildflags.gni - - chrome/browser/ui/BUILD.gn - - chrome/browser/ui/webui/help/sparkle_version_updater_mac.h - - chrome/browser/ui/webui/help/sparkle_version_updater_mac.mm - - chrome/browser/ui/webui/help/version_updater_mac.mm - - third_party/sparkle/ + - chrome/BUILD.gn + - chrome/browser/BUILD.gn + - chrome/browser/mac/chrome_browser_main_extra_parts_mac.h + - chrome/browser/mac/chrome_browser_main_extra_parts_mac.mm + - chrome/browser/mac/sparkle_glue.h + - chrome/browser/mac/sparkle_glue.mm + - chrome/browser/mac/su_updater.h + - chrome/browser/sparkle_buildflags.gni + - chrome/browser/ui/BUILD.gn + - chrome/browser/ui/webui/help/sparkle_version_updater_mac.h + - chrome/browser/ui/webui/help/sparkle_version_updater_mac.mm + - chrome/browser/ui/webui/help/version_updater_mac.mm + - third_party/sparkle/ pin-chat-and-hub: description: pin browseros native panels files: - - chrome/browser/extensions/browseros_extension_constants.h - - chrome/browser/sync/prefs/chrome_syncable_prefs_database.cc - - chrome/browser/ui/actions/browseros_actions_config.h - - chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.cc - - chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h - - chrome/browser/ui/toolbar/toolbar_pref_names.cc - - chrome/browser/ui/toolbar/toolbar_pref_names.h - - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.cc - - chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container.cc + - chrome/browser/extensions/browseros_extension_constants.h + - chrome/browser/sync/prefs/chrome_syncable_prefs_database.cc + - chrome/browser/ui/actions/browseros_actions_config.h + - chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.cc + - chrome/browser/ui/toolbar/pinned_toolbar/pinned_toolbar_actions_model.h + - chrome/browser/ui/toolbar/toolbar_pref_names.cc + - chrome/browser/ui/toolbar/toolbar_pref_names.h + - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.cc + - chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container.cc pin-extensions-toolbar: description: pin browseros extensions to extension toolbar files: - - chrome/browser/extensions/browseros_extension_constants.h - - chrome/browser/extensions/extension_context_menu_model.cc - - chrome/browser/extensions/extension_management.cc - - chrome/browser/ui/actions/browseros_actions_config.h - - chrome/browser/ui/toolbar/toolbar_actions_model.cc - - chrome/browser/ui/views/side_panel/extensions/extension_side_panel_manager.cc + - chrome/browser/extensions/browseros_extension_constants.h + - chrome/browser/extensions/extension_context_menu_model.cc + - chrome/browser/extensions/extension_management.cc + - chrome/browser/ui/actions/browseros_actions_config.h + - chrome/browser/ui/toolbar/toolbar_actions_model.cc + - chrome/browser/ui/views/side_panel/extensions/extension_side_panel_manager.cc + - chrome/browser/ui/views/side_panel/side_panel_action_callback.cc + - chrome/browser/ui/views/side_panel/side_panel_action_callback.h preferences-settings-page: - description: "patch: settings prefs page" + description: 'patch: settings prefs page' files: - - chrome/browser/extensions/api/settings_private/prefs_util.cc - - chrome/browser/prefs/browser_prefs.cc - - chrome/browser/resources/settings/BUILD.gn - - chrome/browser/resources/settings/browseros_prefs_page/browseros_prefs_page.html - - chrome/browser/resources/settings/browseros_prefs_page/browseros_prefs_page.ts - - chrome/browser/resources/settings/route.ts - - chrome/browser/resources/settings/router.ts - - chrome/browser/resources/settings/settings.ts - - chrome/browser/resources/settings/settings_main/settings_main.html - - chrome/browser/resources/settings/settings_main/settings_main.ts - - chrome/browser/resources/settings/settings_menu/settings_menu.html - - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.cc - - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.h - - chrome/common/pref_names.h + - chrome/browser/extensions/api/settings_private/prefs_util.cc + - chrome/browser/prefs/browser_prefs.cc + - chrome/browser/resources/settings/BUILD.gn + - chrome/browser/resources/settings/browseros_prefs_page/browseros_prefs_page.html + - chrome/browser/resources/settings/browseros_prefs_page/browseros_prefs_page.ts + - chrome/browser/resources/settings/route.ts + - chrome/browser/resources/settings/router.ts + - chrome/browser/resources/settings/settings.ts + - chrome/browser/resources/settings/settings_main/settings_main.html + - chrome/browser/resources/settings/settings_main/settings_main.ts + - chrome/browser/resources/settings/settings_menu/settings_menu.html + - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.cc + - chrome/browser/ui/views/toolbar/pinned_action_toolbar_button.h + - chrome/common/pref_names.h ui-fixes: - description: "patch: chromium ui fixes" + description: 'patch: chromium ui fixes' files: - - chrome/browser/chrome_content_browser_client.cc - - chrome/browser/net/profile_network_context_service.cc - - chrome/browser/resources/settings/about_page/about_page.html - - chrome/browser/resources/settings/about_page/about_page.ts - - chrome/browser/resources/settings/reset_page/reset_profile_dialog.html - - chrome/browser/ui/browser_ui_prefs.cc - - chrome/browser/ui/views/chrome_layout_provider.cc - - chrome/browser/ui/views/infobars/infobar_container_view.cc - - chrome/browser/ui/views/new_tab_footer/ - - chrome/browser/ui/webui/new_tab_footer/ - - components/bookmarks/browser/bookmark_utils.cc - - components/content_settings/core/browser/cookie_settings.cc - - components/payments/core/payment_prefs.cc - - components/performance_manager/user_tuning/prefs.cc - - components/search/ntp_features.cc + - chrome/browser/about_flags.cc + - chrome/browser/browser_features.cc + - chrome/browser/browser_features.h + - chrome/browser/chrome_content_browser_client.cc + - chrome/browser/net/profile_network_context_service.cc + - chrome/browser/resources/settings/about_page/about_page.html + - chrome/browser/resources/settings/about_page/about_page.ts + - chrome/browser/resources/settings/reset_page/reset_profile_dialog.html + - chrome/browser/ui/browser_ui_prefs.cc + - chrome/browser/ui/views/chrome_layout_provider.cc + - chrome/browser/ui/views/infobars/infobar_container_view.cc + - chrome/browser/ui/views/new_tab_footer/ + - chrome/browser/ui/views/new_tab_footer/footer_controller.cc + - chrome/browser/ui/webui/new_tab_footer/ + - chrome/browser/ui/webui/new_tab_footer/new_tab_footer_ui.cc + - components/bookmarks/browser/bookmark_utils.cc + - components/content_settings/core/browser/cookie_settings.cc + - components/payments/core/payment_prefs.cc + - components/performance_manager/user_tuning/prefs.cc + - components/search/ntp_features.cc browseros-version: - description: "patch: browseros version" + description: 'patch: browseros version' files: - - base/version_info/BUILD.gn - - base/version_info/version_info.h - - base/version_info/version_info_values.h.version - - chrome/BROWSEROS_VERSION - - chrome/browser/resources/settings/about_page/about_page.html - - chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc + - base/version_info/BUILD.gn + - base/version_info/version_info.h + - base/version_info/version_info_values.h.version + - chrome/BROWSEROS_VERSION + - chrome/browser/resources/settings/about_page/about_page.html + - chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc misc: description: miscellaneous patches files: - - chrome/browser/ui/omnibox/chrome_omnibox_client.cc - - chrome/browser/ui/profiles/profile_error_dialog.cc - - chrome/browser/ui/startup/infobar_utils.cc - - chrome/installer/mini_installer/chrome.release - - chrome/updater/branding.gni - - extensions/browser/process_manager.cc - - extensions/browser/process_manager.h - - third_party/blink/renderer/core/frame/navigator.cc + - chrome/browser/browseros_server/.gitignore + - chrome/browser/ui/omnibox/chrome_omnibox_client.cc + - chrome/browser/ui/profiles/profile_error_dialog.cc + - chrome/browser/ui/startup/infobar_utils.cc + - chrome/installer/mini_installer/chrome.release + - chrome/updater/branding.gni + - extensions/browser/process_manager.cc + - extensions/browser/process_manager.h + - third_party/blink/renderer/core/frame/navigator.cc + flags: + description: adding new flags or udpating existing + files: + - chrome/browser/flag_descriptions.cc + - chrome/browser/flag_descriptions.h + - chrome/browser/ui/browser_window/internal/browser_window_features.cc + - chrome/browser/ui/browser_window/public/browser_window_features.h + keyboard-shorcuts: + description: keyboard shortcuts + files: + - chrome/browser/ui/accelerator_table.cc + mac-sparkle: + description: mac sparkle updater + files: + - third_party/sparkle/BUILD.gn diff --git a/packages/browseros/build/modules/annotate/__init__.py b/packages/browseros/build/modules/annotate/__init__.py new file mode 100644 index 000000000..ced8cc425 --- /dev/null +++ b/packages/browseros/build/modules/annotate/__init__.py @@ -0,0 +1,15 @@ +""" +Annotate module - Create git commits organized by features. + +Provides commands for creating feature-based commits: +- annotate_features: Create commits for all features with modified files +- annotate_single_feature: Create a commit for a specific feature +""" + +from .annotate import annotate_features, annotate_single_feature, AnnotateModule + +__all__ = [ + "annotate_features", + "annotate_single_feature", + "AnnotateModule", +] diff --git a/packages/browseros/build/modules/annotate/annotate.py b/packages/browseros/build/modules/annotate/annotate.py new file mode 100644 index 000000000..296bdbd55 --- /dev/null +++ b/packages/browseros/build/modules/annotate/annotate.py @@ -0,0 +1,239 @@ +""" +Annotate - Create git commits organized by features from features.yaml + +For each feature, checks which files have modifications and creates a commit +with the feature name and description. +""" + +import yaml +from pathlib import Path +from typing import List, Tuple, Optional, Dict + +from ..apply.utils import run_git_command +from ...common.context import Context +from ...common.module import CommandModule, ValidationError +from ...common.utils import log_info, log_error, log_success, log_warning + + +def load_features(features_file: Path) -> Dict: + """Load features from YAML file.""" + try: + with open(features_file, "r") as f: + data = yaml.safe_load(f) + return data.get("features", {}) + except Exception as e: + log_error(f"Failed to load features file: {e}") + return {} + + +def get_modified_files(chromium_src: Path, files: List[str]) -> List[str]: + """Get list of files that have modifications or are untracked. + + Args: + chromium_src: Chromium source directory + files: List of file paths to check + + Returns: + List of file paths that have modifications + """ + modified = [] + + for file_path in files: + full_path = chromium_src / file_path + + if not full_path.exists(): + continue + + result = run_git_command( + ["git", "status", "--porcelain", str(file_path)], + cwd=chromium_src, + ) + + if result.returncode == 0 and result.stdout.strip(): + modified.append(file_path) + + return modified + + +def git_add_and_commit( + chromium_src: Path, files: List[str], commit_message: str +) -> bool: + """Add files and create commit. + + Args: + chromium_src: Chromium source directory + files: List of file paths to add + commit_message: Commit message + + Returns: + True if commit was created successfully + """ + # Add all specified files + for file_path in files: + result = run_git_command( + ["git", "add", str(file_path)], + cwd=chromium_src, + ) + if result.returncode != 0: + log_error(f"Failed to add file: {file_path}") + return False + + # Create commit + result = run_git_command( + ["git", "commit", "-m", commit_message], + cwd=chromium_src, + ) + + if result.returncode != 0: + stderr = result.stderr or "" + if "nothing to commit" in stderr or "nothing added to commit" in stderr: + return False + log_error(f"Failed to commit: {stderr}") + return False + + return True + + +def annotate_features( + ctx: Context, + feature_filter: Optional[str] = None, +) -> Tuple[int, int]: + """Create commits for features with modified files. + + Iterates through features.yaml and creates a commit for each feature + that has modified files in the working tree. + + Args: + ctx: Build context + feature_filter: If specified, only process this feature + + Returns: + Tuple of (commits_created, features_skipped) + """ + features_file = ctx.get_features_yaml_path() + + if not features_file.exists(): + log_error(f"Features file not found: {features_file}") + return 0, 0 + + features = load_features(features_file) + if not features: + log_error("No features found in features.yaml") + return 0, 0 + + # Filter to specific feature if requested + if feature_filter: + if feature_filter not in features: + log_error(f"Feature '{feature_filter}' not found in features.yaml") + return 0, 0 + features = {feature_filter: features[feature_filter]} + + log_info(f"šŸ“‹ Processing {len(features)} feature(s)") + log_info("=" * 60) + + commits_created = 0 + features_skipped = 0 + + for feature_name, feature_data in features.items(): + description = feature_data.get("description", feature_name) + files = feature_data.get("files", []) + + log_info(f"\nšŸ”§ {feature_name}") + log_info(f" {description}") + + if not files: + log_warning(" No files specified, skipping") + features_skipped += 1 + continue + + # Find files with modifications + modified_files = get_modified_files(ctx.chromium_src, files) + + if not modified_files: + log_warning(f" No modified files ({len(files)} files checked)") + features_skipped += 1 + continue + + log_info(f" Found {len(modified_files)} modified file(s)") + + # Create commit + commit_message = f"{feature_name}: {description}" + + if git_add_and_commit(ctx.chromium_src, modified_files, commit_message): + log_success(f" āœ“ Committed {len(modified_files)} file(s)") + commits_created += 1 + else: + log_warning(" No changes staged, skipping commit") + features_skipped += 1 + + return commits_created, features_skipped + + +def annotate_single_feature( + ctx: Context, + feature_name: str, +) -> bool: + """Create a commit for a single feature. + + Args: + ctx: Build context + feature_name: Name of the feature to commit + + Returns: + True if commit was created successfully + """ + commits, _ = annotate_features(ctx, feature_filter=feature_name) + return commits > 0 + + +class AnnotateModule(CommandModule): + """Create git commits organized by features from features.yaml""" + + produces = [] + requires = [] + description = "Create git commits organized by features" + + def validate(self, ctx: Context) -> None: + """Validate git is available and chromium_src exists.""" + import shutil + + if not shutil.which("git"): + raise ValidationError("Git is not available in PATH") + if not ctx.chromium_src.exists(): + raise ValidationError(f"Chromium source not found: {ctx.chromium_src}") + + # Check if it's a git repository + git_dir = ctx.chromium_src / ".git" + if not git_dir.exists(): + raise ValidationError(f"Not a git repository: {ctx.chromium_src}") + + def execute( + self, + ctx: Context, + feature_name: Optional[str] = None, + **kwargs, + ) -> None: + """Execute annotate. + + Args: + ctx: Build context + feature_name: If specified, only annotate this feature + """ + log_info("šŸ—ļø Annotate Features") + log_info("=" * 60) + log_info(f"šŸ“ Chromium source: {ctx.chromium_src}") + log_info(f"šŸ“„ Features file: {ctx.get_features_yaml_path()}") + + commits_created, features_skipped = annotate_features( + ctx, feature_filter=feature_name + ) + + log_info("\n" + "=" * 60) + if commits_created > 0: + log_success(f"āœ“ Created {commits_created} commit(s)") + else: + log_info("No commits created (no modified files found)") + + if features_skipped > 0: + log_info(f" Skipped {features_skipped} feature(s) with no changes") + log_info("=" * 60) diff --git a/packages/browseros/build/modules/apply/apply_all.py b/packages/browseros/build/modules/apply/apply_all.py index ead4f7897..0f2b4beb7 100644 --- a/packages/browseros/build/modules/apply/apply_all.py +++ b/packages/browseros/build/modules/apply/apply_all.py @@ -6,13 +6,12 @@ from typing import List, Tuple, Optional from ...common.context import Context from ...common.module import CommandModule, ValidationError -from ...common.utils import log_info, log_error, log_warning +from ...common.utils import log_info, log_error, log_warning, log_success from .common import find_patch_files, process_patch_list def apply_all_patches( build_ctx: Context, - commit_each: bool = False, dry_run: bool = False, interactive: bool = False, reset_to: Optional[str] = None, @@ -21,7 +20,6 @@ def apply_all_patches( Args: build_ctx: Build context - commit_each: Create a commit after each patch dry_run: Only check if patches would apply interactive: Ask for confirmation before each patch reset_to: Commit to reset files to before applying (optional) @@ -55,7 +53,6 @@ def apply_all_patches( patch_list, build_ctx.chromium_src, patches_dir, - commit_each, dry_run, interactive, reset_to=reset_to, @@ -92,23 +89,34 @@ class ApplyAllModule(CommandModule): self, ctx: Context, interactive: bool = True, - commit: bool = False, reset_to: Optional[str] = None, + annotate: bool = False, **kwargs, ) -> None: """Execute apply all patches Args: interactive: Interactive mode (ask before each patch) - commit: Create git commit after each patch reset_to: Commit to reset files to before applying (optional) + annotate: Create git commits per feature after applying """ applied, failed = apply_all_patches( ctx, - commit_each=commit, dry_run=False, interactive=interactive, reset_to=reset_to, ) if failed: raise RuntimeError(f"Failed to apply {len(failed)} patches") + + # Run annotate if requested + if annotate: + from ..annotate import annotate_features + + log_info("\n" + "=" * 60) + log_info("šŸ—ļø Creating feature-based commits...") + commits, skipped = annotate_features(ctx) + if commits > 0: + log_success(f"āœ“ Created {commits} commit(s)") + else: + log_info("No commits created (no modified files found)") diff --git a/packages/browseros/build/modules/apply/apply_feature.py b/packages/browseros/build/modules/apply/apply_feature.py index 55e5fa018..84870e7ee 100644 --- a/packages/browseros/build/modules/apply/apply_feature.py +++ b/packages/browseros/build/modules/apply/apply_feature.py @@ -7,14 +7,13 @@ from typing import List, Tuple, Optional from ...common.context import Context from ...common.module import CommandModule, ValidationError -from ...common.utils import log_info, log_error, log_warning +from ...common.utils import log_info, log_error, log_warning, log_success from .common import process_patch_list def apply_feature_patches( build_ctx: Context, feature_name: str, - commit_each: bool = False, dry_run: bool = False, reset_to: Optional[str] = None, ) -> Tuple[int, List[str]]: @@ -23,7 +22,6 @@ def apply_feature_patches( Args: build_ctx: Build context feature_name: Name of the feature - commit_each: Create a commit after each patch dry_run: Only check if patches would apply reset_to: Commit to reset files to before applying (optional) @@ -71,10 +69,8 @@ def apply_feature_patches( patch_list, build_ctx.chromium_src, patches_dir, - commit_each, dry_run, interactive=False, # Feature patches don't support interactive mode - feature_name=feature_name, reset_to=reset_to, ) @@ -110,8 +106,8 @@ class ApplyFeatureModule(CommandModule): ctx: Context, feature_name: str, interactive: bool = True, - commit: bool = False, reset_to: Optional[str] = None, + annotate: bool = False, **kwargs, ) -> None: """Execute apply feature patches @@ -119,13 +115,12 @@ class ApplyFeatureModule(CommandModule): Args: feature_name: Name of the feature to apply interactive: Interactive mode (ask before each patch) - commit: Create git commit after applying reset_to: Commit to reset files to before applying (optional) + annotate: Create git commit for this feature after applying """ applied, failed = apply_feature_patches( ctx, feature_name, - commit_each=commit, dry_run=False, reset_to=reset_to, ) @@ -133,3 +128,14 @@ class ApplyFeatureModule(CommandModule): raise RuntimeError( f"Failed to apply {len(failed)} patches for feature '{feature_name}'" ) + + # Run annotate for this specific feature if requested + if annotate: + from ..annotate import annotate_single_feature + + log_info("\n" + "=" * 60) + log_info(f"šŸ—ļø Creating commit for feature '{feature_name}'...") + if annotate_single_feature(ctx, feature_name): + log_success(f"āœ“ Created commit for '{feature_name}'") + else: + log_info("No commit created (no modified files found)") diff --git a/packages/browseros/build/modules/apply/common.py b/packages/browseros/build/modules/apply/common.py index fac8de8d2..befbbcbd6 100644 --- a/packages/browseros/build/modules/apply/common.py +++ b/packages/browseros/build/modules/apply/common.py @@ -153,10 +153,8 @@ def process_patch_list( patch_list: List[Tuple[Path, str]], chromium_src: Path, patches_dir: Path, - commit_each: bool = False, dry_run: bool = False, interactive: bool = False, - feature_name: Optional[str] = None, reset_to: Optional[str] = None, ) -> Tuple[int, List[str]]: """Process a list of patches. @@ -165,10 +163,8 @@ def process_patch_list( patch_list: List of (patch_path, display_name) tuples chromium_src: Chromium source directory patches_dir: Base directory for relative path display - commit_each: Create a commit after each patch dry_run: Only check if patches would apply interactive: Ask for confirmation before each patch - feature_name: Optional feature name for commit messages reset_to: Commit to reset files to before applying (optional) Returns: @@ -218,8 +214,6 @@ def process_patch_list( if success: applied += 1 - if commit_each and not dry_run: - create_patch_commit(display_name, chromium_src, feature_name) else: failed.append(display_name) diff --git a/packages/browseros/build/modules/extract/common.py b/packages/browseros/build/modules/extract/common.py index afe763992..b26d22a29 100644 --- a/packages/browseros/build/modules/extract/common.py +++ b/packages/browseros/build/modules/extract/common.py @@ -6,7 +6,7 @@ Contains core extraction logic used by extract_commit and extract_range. import click from pathlib import Path -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple from ...common.context import Context from ...common.utils import log_info, log_error, log_warning @@ -50,11 +50,16 @@ def write_patches( file_patches: Dict[str, FilePatch], verbose: bool, include_binary: bool, -) -> int: - """Write patches to disk""" +) -> Tuple[int, List[str]]: + """Write patches to disk. + + Returns: + Tuple of (success_count, list of successfully extracted file paths) + """ success_count = 0 fail_count = 0 skip_count = 0 + extracted_files: List[str] = [] for file_path, patch in file_patches.items(): if verbose: @@ -66,6 +71,7 @@ def write_patches( # Create deletion marker if create_deletion_marker(ctx, file_path): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 @@ -74,6 +80,7 @@ def write_patches( # Create binary marker if create_binary_marker(ctx, file_path, patch.operation): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 else: @@ -86,6 +93,7 @@ def write_patches( # If there are changes beyond the rename if write_patch_file(ctx, file_path, patch.patch_content): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 else: @@ -98,6 +106,7 @@ def write_patches( marker_path.write_text(marker_content) log_info(f" Rename marked: {file_path}") success_count += 1 + extracted_files.append(file_path) except Exception as e: log_error(f" Failed to mark rename: {e}") fail_count += 1 @@ -107,6 +116,7 @@ def write_patches( if patch.patch_content: if write_patch_file(ctx, file_path, patch.patch_content): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 else: @@ -121,7 +131,7 @@ def write_patches( if skip_count > 0: log_info(f"Skipped {skip_count} files") - return success_count + return success_count, extracted_files def extract_normal( @@ -130,8 +140,12 @@ def extract_normal( verbose: bool, force: bool, include_binary: bool, -) -> int: - """Extract patches normally (diff against parent)""" +) -> Tuple[int, List[str]]: + """Extract patches normally (diff against parent). + + Returns: + Tuple of (count, list of extracted file paths) + """ from .utils import GitError # Get diff against parent @@ -149,11 +163,11 @@ def extract_normal( if not file_patches: log_warning("No changes found in commit") - return 0 + return 0, [] # Check for existing patches if not force and not check_overwrite(ctx, file_patches, verbose): - return 0 + return 0, [] # Write patches return write_patches(ctx, file_patches, verbose, include_binary) @@ -166,15 +180,19 @@ def extract_with_base( verbose: bool, force: bool, include_binary: bool, -) -> int: - """Extract patches with custom base (full diff from base for files in commit)""" +) -> Tuple[int, List[str]]: + """Extract patches with custom base (full diff from base for files in commit). + + Returns: + Tuple of (count, list of extracted file paths) + """ # Step 1: Get list of files changed in the commit changed_files = get_commit_changed_files(commit_hash, ctx.chromium_src) if not changed_files: log_warning(f"No files changed in commit {commit_hash}") - return 0 + return 0, [] if verbose: log_info(f"Files changed in {commit_hash}: {len(changed_files)}") @@ -243,13 +261,13 @@ def extract_with_base( if not file_patches: log_warning("No patches to extract") - return 0 + return 0, [] log_info(f"Extracting {len(file_patches)} patches with base {base}") # Check for existing patches if not force and not check_overwrite(ctx, file_patches, verbose): - return 0 + return 0, [] # Write patches return write_patches(ctx, file_patches, verbose, include_binary) diff --git a/packages/browseros/build/modules/extract/extract_commit.py b/packages/browseros/build/modules/extract/extract_commit.py index 3c700bd39..7efbe18f9 100644 --- a/packages/browseros/build/modules/extract/extract_commit.py +++ b/packages/browseros/build/modules/extract/extract_commit.py @@ -3,7 +3,7 @@ Extract Commit - Extract patches from a single git commit. """ from pathlib import Path -from typing import Optional +from typing import List, Optional, Tuple from ...common.context import Context from ...common.module import CommandModule, ValidationError @@ -24,7 +24,7 @@ def extract_single_commit( force: bool = False, include_binary: bool = False, base: Optional[str] = None, -) -> int: +) -> Tuple[int, List[str]]: """Extract patches from a single commit Args: @@ -36,7 +36,7 @@ def extract_single_commit( base: If provided, extract full diff from base for files in commit Returns: - Number of patches successfully extracted + Tuple of (count, list of extracted file paths) """ # Step 1: Validate commit if not validate_commit_exists(commit_hash, ctx.chromium_src): @@ -82,6 +82,7 @@ class ExtractCommitModule(CommandModule): force: bool = False, include_binary: bool = False, base: Optional[str] = None, + feature: bool = False, ) -> None: """Execute extract commit @@ -93,9 +94,10 @@ class ExtractCommitModule(CommandModule): force: Overwrite existing patches include_binary: Include binary files base: Extract full diff from base commit for files in COMMIT + feature: Prompt to add extracted files to a feature in features.yaml """ try: - count = extract_single_commit( + count, extracted_files = extract_single_commit( ctx, commit_hash=commit, verbose=verbose, @@ -107,5 +109,28 @@ class ExtractCommitModule(CommandModule): log_warning(f"No patches extracted from {commit}") else: log_success(f"Successfully extracted {count} patches from {commit}") + + # Handle --feature flag + if feature and extracted_files: + self._add_to_feature(ctx, commit, extracted_files) + except GitError as e: raise RuntimeError(f"Git error: {e}") + + def _add_to_feature(self, ctx: Context, commit: str, files: List[str]) -> None: + """Prompt user to add extracted files to a feature.""" + from ..feature import prompt_feature_selection, add_files_to_feature + from .utils import get_commit_info + + # Get commit info for context + commit_info = get_commit_info(commit, ctx.chromium_src) + commit_message = commit_info.get("subject") if commit_info else None + + # Prompt for feature selection + result = prompt_feature_selection(ctx, commit[:12], commit_message) + if result is None: + log_warning("Skipped adding files to feature") + return + + feature_name, description = result + add_files_to_feature(ctx, feature_name, description, files) diff --git a/packages/browseros/build/modules/extract/extract_range.py b/packages/browseros/build/modules/extract/extract_range.py index b2cee2a5f..a9d733ecc 100644 --- a/packages/browseros/build/modules/extract/extract_range.py +++ b/packages/browseros/build/modules/extract/extract_range.py @@ -4,7 +4,7 @@ Extract Range - Extract patches from a range of git commits. import click from pathlib import Path -from typing import Optional +from typing import List, Optional, Tuple from ...common.context import Context from ...common.module import CommandModule, ValidationError @@ -33,11 +33,11 @@ def extract_commit_range( force: bool = False, include_binary: bool = False, custom_base: Optional[str] = None, -) -> int: +) -> Tuple[int, List[str]]: """Extract patches from a commit range as a single cumulative diff Returns: - Number of patches successfully extracted + Tuple of (count, list of extracted file paths) """ # Step 1: Validate commits if not validate_commit_exists(base_commit, ctx.chromium_src): @@ -56,7 +56,7 @@ def extract_commit_range( if commit_count == 0: log_warning(f"No commits between {base_commit} and {head_commit}") - return 0 + return 0, [] log_info(f"Processing {commit_count} commits") @@ -80,7 +80,7 @@ def extract_commit_range( if not changed_files: log_warning("No files changed in range") - return 0 + return 0, [] log_info(f"Found {len(changed_files)} files changed in range") @@ -107,15 +107,16 @@ def extract_commit_range( if not file_patches: log_warning("No changes found in commit range") - return 0 + return 0, [] # Check for existing patches if not force and not check_overwrite(ctx, file_patches, verbose): - return 0 + return 0, [] success_count = 0 fail_count = 0 skip_count = 0 + extracted_files: List[str] = [] # Process with progress indicator with click.progressbar( @@ -129,6 +130,7 @@ def extract_commit_range( if patch.operation == FileOperation.DELETE: if create_deletion_marker(ctx, file_path): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 @@ -136,6 +138,7 @@ def extract_commit_range( if include_binary: if create_binary_marker(ctx, file_path, patch.operation): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 else: @@ -144,6 +147,7 @@ def extract_commit_range( elif patch.patch_content: if write_patch_file(ctx, file_path, patch.patch_content): success_count += 1 + extracted_files.append(file_path) else: fail_count += 1 else: @@ -157,7 +161,7 @@ def extract_commit_range( if skip_count > 0: log_info(f"Skipped {skip_count} files") - return success_count + return success_count, extracted_files def extract_commits_individually( @@ -168,13 +172,13 @@ def extract_commits_individually( force: bool = False, include_binary: bool = False, custom_base: Optional[str] = None, -) -> int: +) -> Tuple[int, List[str]]: """Extract patches from each commit in a range individually This preserves commit boundaries and can help with conflict resolution. Returns: - Total number of patches successfully extracted + Tuple of (count, list of extracted file paths) """ # Validate custom base if provided if custom_base and not validate_commit_exists(custom_base, ctx.chromium_src): @@ -193,13 +197,14 @@ def extract_commits_individually( if not commits: log_warning(f"No commits between {base_commit} and {head_commit}") - return 0 + return 0, [] log_info(f"Extracting patches from {len(commits)} commits individually") if custom_base: log_info(f"Using custom base: {custom_base}") total_extracted = 0 + all_extracted_files: List[str] = [] failed_commits = [] with click.progressbar( @@ -209,7 +214,7 @@ def extract_commits_individually( try: if custom_base: # Use extract_with_base for full diff from custom base - extracted = extract_with_base( + extracted, files = extract_with_base( ctx, commit, custom_base, @@ -219,7 +224,7 @@ def extract_commits_individually( ) else: # Normal extraction from parent - extracted = extract_single_commit( + extracted, files = extract_single_commit( ctx, commit, verbose=False, @@ -227,6 +232,7 @@ def extract_commits_individually( include_binary=include_binary, ) total_extracted += extracted + all_extracted_files.extend(files) except GitError as e: failed_commits.append((commit, str(e))) if verbose: @@ -239,7 +245,9 @@ def extract_commits_individually( if len(failed_commits) > 5: log_warning(f" ... and {len(failed_commits) - 5} more") - return total_extracted + # Deduplicate files (same file may appear in multiple commits) + unique_files = list(dict.fromkeys(all_extracted_files)) + return total_extracted, unique_files class ExtractRangeModule(CommandModule): @@ -268,6 +276,7 @@ class ExtractRangeModule(CommandModule): include_binary: bool = False, squash: bool = False, base: Optional[str] = None, + feature: bool = False, ) -> None: """Execute extract range @@ -281,10 +290,11 @@ class ExtractRangeModule(CommandModule): include_binary: Include binary files squash: Squash all commits into single patches base: Use different base for diff (full diff from base for files in range) + feature: Prompt to add extracted files to a feature in features.yaml """ try: if squash: - count = extract_commit_range( + count, extracted_files = extract_commit_range( ctx, base_commit=start, head_commit=end, @@ -294,7 +304,7 @@ class ExtractRangeModule(CommandModule): custom_base=base, ) else: - count = extract_commits_individually( + count, extracted_files = extract_commits_individually( ctx, base_commit=start, head_commit=end, @@ -307,5 +317,28 @@ class ExtractRangeModule(CommandModule): log_warning(f"No patches extracted from range {start}..{end}") else: log_success(f"Successfully extracted {count} patches from {start}..{end}") + + # Handle --feature flag + if feature and extracted_files: + self._add_to_feature(ctx, end, extracted_files) + except GitError as e: raise RuntimeError(f"Git error: {e}") + + def _add_to_feature(self, ctx: Context, commit: str, files: List[str]) -> None: + """Prompt user to add extracted files to a feature.""" + from ..feature import prompt_feature_selection, add_files_to_feature + from .utils import get_commit_info + + # Get commit info for context (use the end commit) + commit_info = get_commit_info(commit, ctx.chromium_src) + commit_message = commit_info.get("subject") if commit_info else None + + # Prompt for feature selection + result = prompt_feature_selection(ctx, commit[:12], commit_message) + if result is None: + log_warning("Skipped adding files to feature") + return + + feature_name, description = result + add_files_to_feature(ctx, feature_name, description, files) diff --git a/packages/browseros/build/modules/feature/__init__.py b/packages/browseros/build/modules/feature/__init__.py index 423d42718..93685b345 100644 --- a/packages/browseros/build/modules/feature/__init__.py +++ b/packages/browseros/build/modules/feature/__init__.py @@ -5,6 +5,9 @@ Provides commands for managing features: - add_feature: Add files from a commit to a feature - list_features: List all defined features - show_feature: Show details of a specific feature +- prompt_feature_selection: Interactive feature selection for extract commands +- add_files_to_feature: Add files to a feature (with duplicate handling) +- classify_files: Classify unclassified patch files into features """ from .feature import ( @@ -14,6 +17,13 @@ from .feature import ( ListFeaturesModule, show_feature, ShowFeatureModule, + ClassifyFeaturesModule, +) +from .select import ( + prompt_feature_selection, + add_files_to_feature, + classify_files, + get_unclassified_files, ) __all__ = [ @@ -23,4 +33,9 @@ __all__ = [ "ListFeaturesModule", "show_feature", "ShowFeatureModule", + "ClassifyFeaturesModule", + "prompt_feature_selection", + "add_files_to_feature", + "classify_files", + "get_unclassified_files", ] diff --git a/packages/browseros/build/modules/feature/feature.py b/packages/browseros/build/modules/feature/feature.py index 4468888d7..751b9caeb 100644 --- a/packages/browseros/build/modules/feature/feature.py +++ b/packages/browseros/build/modules/feature/feature.py @@ -151,4 +151,32 @@ class AddFeatureModule(CommandModule): def execute(self, ctx: Context, feature_name: str, commit: str, description: Optional[str] = None, **kwargs) -> None: success = add_feature(ctx, feature_name, commit, description) if not success: - raise RuntimeError(f"Failed to add feature '{feature_name}'") \ No newline at end of file + raise RuntimeError(f"Failed to add feature '{feature_name}'") + + +class ClassifyFeaturesModule(CommandModule): + """Classify unclassified patch files into features""" + produces = [] + requires = [] + description = "Classify unclassified patch files into features" + + def validate(self, ctx: Context) -> None: + """Validate patches directory exists""" + patches_dir = ctx.get_patches_dir() + if not patches_dir.exists(): + raise ValidationError(f"Patches directory not found: {patches_dir}") + + def execute(self, ctx: Context, **kwargs) -> None: + from .select import classify_files, get_unclassified_files + + # Show summary first + unclassified = get_unclassified_files(ctx) + if not unclassified: + log_success("All patch files are already classified!") + return + + log_info(f"Found {len(unclassified)} unclassified patch file(s)") + log_info("") + + # Run classification + classified, skipped = classify_files(ctx) \ No newline at end of file diff --git a/packages/browseros/build/modules/feature/select.py b/packages/browseros/build/modules/feature/select.py new file mode 100644 index 000000000..bb3358f52 --- /dev/null +++ b/packages/browseros/build/modules/feature/select.py @@ -0,0 +1,396 @@ +""" +Feature selection utilities for interactive feature assignment. + +Provides functions to prompt users to select or create features +and add files to them. +""" + +import yaml +from pathlib import Path +from typing import List, Optional, Dict, Tuple, Set + +from ...common.context import Context +from ...common.utils import log_info, log_success, log_warning, log_error + + +def load_features_yaml(features_file: Path) -> Dict: + """Load features from YAML file.""" + if not features_file.exists(): + return {"version": "1.0", "features": {}} + + with open(features_file, "r") as f: + content = yaml.safe_load(f) + if not content: + return {"version": "1.0", "features": {}} + return content + + +def save_features_yaml(features_file: Path, data: Dict) -> None: + """Save features to YAML file.""" + with open(features_file, "w") as f: + yaml.safe_dump(data, f, sort_keys=False, default_flow_style=False) + + +def prompt_feature_selection( + ctx: Context, + commit_hash: Optional[str] = None, + commit_message: Optional[str] = None, +) -> Optional[Tuple[str, str]]: + """Prompt user to select an existing feature or create a new one. + + Args: + ctx: Build context + commit_hash: Optional commit hash for display + commit_message: Optional commit message for display/defaults + + Returns: + Tuple of (feature_name, description) or None if cancelled + """ + features_file = ctx.get_features_yaml_path() + data = load_features_yaml(features_file) + features = data.get("features", {}) + + # Display commit info if available + if commit_hash or commit_message: + log_info("") + log_info("=" * 60) + if commit_hash: + log_info(f"Commit: {commit_hash[:12]}") + if commit_message: + log_info(f"Message: {commit_message}") + log_info("=" * 60) + + # Display numbered list of features + log_info("") + log_info("Select a feature to add files to:") + log_info("-" * 40) + + feature_list = list(features.keys()) + for i, name in enumerate(feature_list, 1): + desc = features[name].get("description", name) + file_count = len(features[name].get("files", [])) + log_info(f" {i}) {desc} ({file_count} files)") + + # Add "new feature" option + new_option = len(feature_list) + 1 + log_info(f" {new_option}) [Add new feature]") + log_info("") + + # Get user selection + while True: + try: + choice = input(f"Enter choice (1-{new_option}): ").strip() + if not choice: + log_warning("Cancelled") + return None + + choice_num = int(choice) + if choice_num < 1 or choice_num > new_option: + log_warning(f"Please enter a number between 1 and {new_option}") + continue + + break + except ValueError: + log_warning("Please enter a valid number") + continue + except (KeyboardInterrupt, EOFError): + log_warning("\nCancelled") + return None + + # Handle selection + if choice_num == new_option: + # Create new feature + return prompt_new_feature(commit_message) + else: + # Selected existing feature + feature_name = feature_list[choice_num - 1] + description = features[feature_name].get("description", "") + return (feature_name, description) + + +def prompt_new_feature(default_description: Optional[str] = None) -> Optional[Tuple[str, str]]: + """Prompt user to create a new feature. + + Args: + default_description: Optional default description (e.g., from commit message) + + Returns: + Tuple of (feature_name, description) or None if cancelled + """ + log_info("") + log_info("Creating new feature:") + log_info("-" * 40) + + try: + # Get feature name + feature_name = input("Feature name: ").strip() + if not feature_name: + log_warning("Cancelled - no feature name provided") + return None + + # Sanitize feature name (lowercase, hyphens instead of spaces) + feature_name = feature_name.lower().replace(" ", "-") + + # Get description + if default_description: + desc_prompt = f"Description [{default_description}]: " + else: + desc_prompt = "Description: " + + description = input(desc_prompt).strip() + if not description and default_description: + description = default_description + + if not description: + description = f"Feature: {feature_name}" + + return (feature_name, description) + + except (KeyboardInterrupt, EOFError): + log_warning("\nCancelled") + return None + + +def add_files_to_feature( + ctx: Context, + feature_name: str, + description: str, + files: List[str], +) -> int: + """Add files to a feature in features.yaml, avoiding duplicates. + + Args: + ctx: Build context + feature_name: Name of the feature + description: Feature description + files: List of file paths to add + + Returns: + Number of new files added (excludes duplicates) + """ + features_file = ctx.get_features_yaml_path() + data = load_features_yaml(features_file) + + if "features" not in data: + data["features"] = {} + + features = data["features"] + + # Get or create feature entry + if feature_name in features: + existing_files = set(features[feature_name].get("files", [])) + # Keep existing description if present + if not features[feature_name].get("description"): + features[feature_name]["description"] = description + else: + existing_files = set() + features[feature_name] = { + "description": description, + "files": [], + } + + # Add new files, avoiding duplicates + new_files = [] + duplicate_files = [] + + for file_path in files: + if file_path in existing_files: + duplicate_files.append(file_path) + else: + new_files.append(file_path) + existing_files.add(file_path) + + # Update feature with merged file list + features[feature_name]["files"] = sorted(existing_files) + + # Save to file + save_features_yaml(features_file, data) + + # Log results + if new_files: + log_success(f"Added {len(new_files)} file(s) to feature '{feature_name}'") + for f in new_files[:5]: + log_info(f" + {f}") + if len(new_files) > 5: + log_info(f" ... and {len(new_files) - 5} more") + + if duplicate_files: + log_warning(f"Skipped {len(duplicate_files)} duplicate file(s)") + for f in duplicate_files[:3]: + log_info(f" ~ {f}") + if len(duplicate_files) > 3: + log_info(f" ... and {len(duplicate_files) - 3} more") + + return len(new_files) + + +def get_all_patch_files(ctx: Context) -> List[str]: + """Get all patch files from chromium_patches/ directory. + + Returns: + List of file paths (relative to chromium_patches/) + """ + patches_dir = ctx.get_patches_dir() + if not patches_dir.exists(): + return [] + + patch_files = [] + for patch_path in patches_dir.rglob("*"): + if patch_path.is_file(): + # Get relative path from patches_dir + rel_path = str(patch_path.relative_to(patches_dir)) + patch_files.append(rel_path) + + return sorted(patch_files) + + +def get_all_classified_files(ctx: Context) -> Set[str]: + """Get all files that are already classified in features.yaml. + + Returns: + Set of file paths + """ + features_file = ctx.get_features_yaml_path() + data = load_features_yaml(features_file) + features = data.get("features", {}) + + classified = set() + for feature_data in features.values(): + files = feature_data.get("files", []) + classified.update(files) + + return classified + + +def get_unclassified_files(ctx: Context) -> List[str]: + """Get list of patch files not in any feature. + + Returns: + List of unclassified file paths + """ + all_patches = set(get_all_patch_files(ctx)) + classified = get_all_classified_files(ctx) + unclassified = all_patches - classified + return sorted(unclassified) + + +def classify_files(ctx: Context) -> Tuple[int, int]: + """Interactively classify unclassified patch files into features. + + Goes through each unclassified file one-by-one and prompts user + to select a feature or create a new one. + + Returns: + Tuple of (files_classified, files_skipped) + """ + unclassified = get_unclassified_files(ctx) + + if not unclassified: + log_success("All patch files are already classified!") + return 0, 0 + + log_info(f"Found {len(unclassified)} unclassified file(s)") + log_info("=" * 60) + log_info("Press Ctrl+C to stop at any time") + log_info("") + + classified_count = 0 + skipped_count = 0 + + for i, file_path in enumerate(unclassified, 1): + log_info(f"\n[{i}/{len(unclassified)}] {file_path}") + log_info("-" * 40) + + try: + # Prompt for feature selection (no commit context for classify) + result = prompt_feature_selection_for_file(ctx, file_path) + + if result is None: + log_warning("Skipped") + skipped_count += 1 + continue + + feature_name, description = result + add_files_to_feature(ctx, feature_name, description, [file_path]) + classified_count += 1 + + except KeyboardInterrupt: + log_info("\n\nStopped by user") + break + + log_info("") + log_info("=" * 60) + log_success(f"Classified {classified_count} file(s)") + if skipped_count > 0: + log_info(f"Skipped {skipped_count} file(s)") + remaining = len(unclassified) - classified_count - skipped_count + if remaining > 0: + log_info(f"Remaining: {remaining} file(s)") + + return classified_count, skipped_count + + +def prompt_feature_selection_for_file( + ctx: Context, + file_path: str, +) -> Optional[Tuple[str, str]]: + """Prompt user to select a feature for a single file. + + Simplified version of prompt_feature_selection for classify workflow. + + Args: + ctx: Build context + file_path: The file being classified + + Returns: + Tuple of (feature_name, description) or None if skipped + """ + features_file = ctx.get_features_yaml_path() + data = load_features_yaml(features_file) + features = data.get("features", {}) + + if not features: + log_info("No features defined yet. Create a new one:") + return prompt_new_feature() + + # Display numbered list of features + feature_list = list(features.keys()) + for i, name in enumerate(feature_list, 1): + desc = features[name].get("description", name) + file_count = len(features[name].get("files", [])) + log_info(f" {i}) {desc} ({file_count} files)") + + # Add options + new_option = len(feature_list) + 1 + skip_option = len(feature_list) + 2 + log_info(f" {new_option}) [Add new feature]") + log_info(f" {skip_option}) [Skip this file]") + + # Get user selection + while True: + try: + choice = input(f"Choice (1-{skip_option}): ").strip() + if not choice: + return None + + choice_num = int(choice) + if choice_num < 1 or choice_num > skip_option: + log_warning(f"Please enter 1-{skip_option}") + continue + + break + except ValueError: + log_warning("Enter a valid number") + continue + except (KeyboardInterrupt, EOFError): + raise KeyboardInterrupt + + # Handle selection + if choice_num == skip_option: + return None + elif choice_num == new_option: + return prompt_new_feature() + else: + feature_name = feature_list[choice_num - 1] + description = features[feature_name].get("description", "") + return (feature_name, description) diff --git a/packages/browseros/build/modules/patches/patches.py b/packages/browseros/build/modules/patches/patches.py index 0663ff2f9..132bc49ae 100644 --- a/packages/browseros/build/modules/patches/patches.py +++ b/packages/browseros/build/modules/patches/patches.py @@ -24,13 +24,11 @@ class PatchesModule(CommandModule): def execute(self, ctx: Context) -> None: log_info("\n🩹 Applying patches...") - if not apply_patches_impl(ctx, interactive=False, commit_each=False): + if not apply_patches_impl(ctx, interactive=False): raise RuntimeError("Failed to apply patches") -def apply_patches_impl( - ctx: Context, interactive: bool = False, commit_each: bool = False -) -> bool: +def apply_patches_impl(ctx: Context, interactive: bool = False) -> bool: """Apply patches using the dev CLI patch system Returns: @@ -53,7 +51,6 @@ def apply_patches_impl( # Call the dev CLI function directly _, failed = apply_all_patches( build_ctx=ctx, - commit_each=commit_each, dry_run=False, interactive=interactive, )