roadmap-cli 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roadmap/__init__.py +27 -0
- roadmap/adapters/__init__.py +1 -0
- roadmap/adapters/base_paginated_adapter.py +117 -0
- roadmap/adapters/cli/__init__.py +249 -0
- roadmap/adapters/cli/analysis/__init__.py +5 -0
- roadmap/adapters/cli/analysis/commands.py +370 -0
- roadmap/adapters/cli/analysis/presenter.py +125 -0
- roadmap/adapters/cli/archive_operations.py +86 -0
- roadmap/adapters/cli/cli_command_helpers.py +109 -0
- roadmap/adapters/cli/cli_confirmations.py +74 -0
- roadmap/adapters/cli/cli_error_handlers.py +146 -0
- roadmap/adapters/cli/cli_validators.py +39 -0
- roadmap/adapters/cli/commands/sync_status.py +308 -0
- roadmap/adapters/cli/comment/__init__.py +5 -0
- roadmap/adapters/cli/comment/commands.py +106 -0
- roadmap/adapters/cli/config/__init__.py +5 -0
- roadmap/adapters/cli/config/commands.py +198 -0
- roadmap/adapters/cli/console_exports.py +10 -0
- roadmap/adapters/cli/core.py +26 -0
- roadmap/adapters/cli/crud/__init__.py +47 -0
- roadmap/adapters/cli/crud/base_archive.py +208 -0
- roadmap/adapters/cli/crud/base_create.py +154 -0
- roadmap/adapters/cli/crud/base_delete.py +155 -0
- roadmap/adapters/cli/crud/base_restore.py +283 -0
- roadmap/adapters/cli/crud/base_update.py +193 -0
- roadmap/adapters/cli/crud/crud_helpers.py +206 -0
- roadmap/adapters/cli/crud/crud_utils.py +152 -0
- roadmap/adapters/cli/crud/entity_builders.py +374 -0
- roadmap/adapters/cli/data/__init__.py +8 -0
- roadmap/adapters/cli/data/commands.py +149 -0
- roadmap/adapters/cli/decorators.py +208 -0
- roadmap/adapters/cli/dtos/__init__.py +155 -0
- roadmap/adapters/cli/exception_handler.py +105 -0
- roadmap/adapters/cli/git/__init__.py +8 -0
- roadmap/adapters/cli/git/commands.py +475 -0
- roadmap/adapters/cli/git/handlers/__init__.py +1 -0
- roadmap/adapters/cli/git/handlers/git_authentication_handler.py +124 -0
- roadmap/adapters/cli/git/handlers/git_branch_handler.py +238 -0
- roadmap/adapters/cli/git/handlers/git_connectivity_handler.py +122 -0
- roadmap/adapters/cli/git/handlers/git_hooks_handler.py +105 -0
- roadmap/adapters/cli/git/hooks_config.py +327 -0
- roadmap/adapters/cli/git/status_display.py +158 -0
- roadmap/adapters/cli/health/__init__.py +8 -0
- roadmap/adapters/cli/health/db_integrity.py +204 -0
- roadmap/adapters/cli/health/enhancer.py +227 -0
- roadmap/adapters/cli/health/fixer.py +295 -0
- roadmap/adapters/cli/health/fixers/__init__.py +10 -0
- roadmap/adapters/cli/health/fixers/corrupted_comments_fixer.py +231 -0
- roadmap/adapters/cli/health/fixers/data_integrity_fixer.py +137 -0
- roadmap/adapters/cli/health/fixers/duplicate_issues_fixer.py +138 -0
- roadmap/adapters/cli/health/fixers/folder_structure_fixer.py +114 -0
- roadmap/adapters/cli/health/fixers/label_normalization_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_name_normalization_fixer.py +181 -0
- roadmap/adapters/cli/health/fixers/milestone_naming_compliance_fixer.py +244 -0
- roadmap/adapters/cli/health/fixers/milestone_parse_error_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_validation_fixer.py +211 -0
- roadmap/adapters/cli/health/fixers/old_backups_fixer.py +135 -0
- roadmap/adapters/cli/health/fixers/orphaned_issues_fixer.py +252 -0
- roadmap/adapters/cli/health/formatter.py +250 -0
- roadmap/adapters/cli/health/formatters.py +427 -0
- roadmap/adapters/cli/health/scan.py +293 -0
- roadmap/adapters/cli/init/__init__.py +5 -0
- roadmap/adapters/cli/init/commands.py +587 -0
- roadmap/adapters/cli/init.py +9 -0
- roadmap/adapters/cli/issues/__init__.py +70 -0
- roadmap/adapters/cli/issues/archive.py +137 -0
- roadmap/adapters/cli/issues/archive_class.py +253 -0
- roadmap/adapters/cli/issues/block.py +39 -0
- roadmap/adapters/cli/issues/close.py +216 -0
- roadmap/adapters/cli/issues/comment.py +178 -0
- roadmap/adapters/cli/issues/create.py +162 -0
- roadmap/adapters/cli/issues/delete.py +31 -0
- roadmap/adapters/cli/issues/deps.py +161 -0
- roadmap/adapters/cli/issues/issue_status_helpers.py +111 -0
- roadmap/adapters/cli/issues/link.py +308 -0
- roadmap/adapters/cli/issues/list.py +402 -0
- roadmap/adapters/cli/issues/lookup.py +96 -0
- roadmap/adapters/cli/issues/progress.py +102 -0
- roadmap/adapters/cli/issues/restore.py +96 -0
- roadmap/adapters/cli/issues/restore_class.py +57 -0
- roadmap/adapters/cli/issues/start.py +165 -0
- roadmap/adapters/cli/issues/sync_status.py +374 -0
- roadmap/adapters/cli/issues/unblock.py +43 -0
- roadmap/adapters/cli/issues/unlink.py +104 -0
- roadmap/adapters/cli/issues/update.py +120 -0
- roadmap/adapters/cli/issues/view.py +36 -0
- roadmap/adapters/cli/layout.py +220 -0
- roadmap/adapters/cli/mappers.py +200 -0
- roadmap/adapters/cli/milestones/__init__.py +52 -0
- roadmap/adapters/cli/milestones/archive.py +125 -0
- roadmap/adapters/cli/milestones/archive_class.py +236 -0
- roadmap/adapters/cli/milestones/assign.py +62 -0
- roadmap/adapters/cli/milestones/close.py +177 -0
- roadmap/adapters/cli/milestones/create.py +131 -0
- roadmap/adapters/cli/milestones/delete.py +27 -0
- roadmap/adapters/cli/milestones/kanban.py +123 -0
- roadmap/adapters/cli/milestones/list.py +87 -0
- roadmap/adapters/cli/milestones/recalculate.py +79 -0
- roadmap/adapters/cli/milestones/restore.py +87 -0
- roadmap/adapters/cli/milestones/restore_class.py +99 -0
- roadmap/adapters/cli/milestones/update.py +61 -0
- roadmap/adapters/cli/milestones/view.py +142 -0
- roadmap/adapters/cli/output_manager.py +319 -0
- roadmap/adapters/cli/presentation/base_presenter.py +121 -0
- roadmap/adapters/cli/presentation/cleanup_presenter.py +235 -0
- roadmap/adapters/cli/presentation/core_initialization_presenter.py +277 -0
- roadmap/adapters/cli/presentation/crud_presenter.py +134 -0
- roadmap/adapters/cli/presentation/daily_summary_presenter.py +266 -0
- roadmap/adapters/cli/presentation/issue_presenter.py +258 -0
- roadmap/adapters/cli/presentation/milestone_list_presenter.py +135 -0
- roadmap/adapters/cli/presentation/milestone_presenter.py +297 -0
- roadmap/adapters/cli/presentation/project_presenter.py +233 -0
- roadmap/adapters/cli/presentation/project_status_presenter.py +186 -0
- roadmap/adapters/cli/presentation/table_builders.py +95 -0
- roadmap/adapters/cli/projects/__init__.py +42 -0
- roadmap/adapters/cli/projects/archive.py +127 -0
- roadmap/adapters/cli/projects/archive_class.py +96 -0
- roadmap/adapters/cli/projects/close.py +94 -0
- roadmap/adapters/cli/projects/create.py +50 -0
- roadmap/adapters/cli/projects/delete.py +27 -0
- roadmap/adapters/cli/projects/list.py +227 -0
- roadmap/adapters/cli/projects/restore.py +87 -0
- roadmap/adapters/cli/projects/restore_class.py +56 -0
- roadmap/adapters/cli/projects/update.py +57 -0
- roadmap/adapters/cli/projects/view.py +157 -0
- roadmap/adapters/cli/services/__init__.py +1 -0
- roadmap/adapters/cli/services/daily_summary_service.py +313 -0
- roadmap/adapters/cli/services/export_manager.py +268 -0
- roadmap/adapters/cli/services/milestone_list_service.py +266 -0
- roadmap/adapters/cli/services/project_initialization_service.py +29 -0
- roadmap/adapters/cli/services/project_status_service.py +462 -0
- roadmap/adapters/cli/services/sync_service.py +62 -0
- roadmap/adapters/cli/status.py +408 -0
- roadmap/adapters/cli/styling.py +21 -0
- roadmap/adapters/cli/sync.py +1117 -0
- roadmap/adapters/cli/sync_context.py +432 -0
- roadmap/adapters/cli/sync_handlers/__init__.py +77 -0
- roadmap/adapters/cli/sync_handlers/apply_ops.py +522 -0
- roadmap/adapters/cli/sync_handlers/baseline_ops.py +366 -0
- roadmap/adapters/cli/sync_handlers/conflict_ops.py +161 -0
- roadmap/adapters/cli/sync_handlers/dry_run_display.py +237 -0
- roadmap/adapters/cli/sync_handlers/interactive_resolver.py +396 -0
- roadmap/adapters/cli/sync_handlers/progress_tracker.py +128 -0
- roadmap/adapters/cli/sync_metrics_command.py +190 -0
- roadmap/adapters/cli/sync_presenter.py +58 -0
- roadmap/adapters/cli/sync_validation.py +295 -0
- roadmap/adapters/cli/today.py +52 -0
- roadmap/adapters/cli/utils/click_options.py +38 -0
- roadmap/adapters/git/__init__.py +1 -0
- roadmap/adapters/git/git.py +208 -0
- roadmap/adapters/git/git_branch_manager.py +400 -0
- roadmap/adapters/git/git_command_executor.py +56 -0
- roadmap/adapters/git/git_commit_analyzer.py +334 -0
- roadmap/adapters/git/git_hooks.py +22 -0
- roadmap/adapters/git/git_hooks_manager.py +455 -0
- roadmap/adapters/git/git_repository_info.py +65 -0
- roadmap/adapters/git/hook_installer.py +106 -0
- roadmap/adapters/git/hook_registry.py +97 -0
- roadmap/adapters/git/hook_script_generator.py +58 -0
- roadmap/adapters/git/sync_monitor.py +409 -0
- roadmap/adapters/git/workflow_automation.py +286 -0
- roadmap/adapters/github/__init__.py +1 -0
- roadmap/adapters/github/github.py +360 -0
- roadmap/adapters/github/handlers/__init__.py +17 -0
- roadmap/adapters/github/handlers/base.py +337 -0
- roadmap/adapters/github/handlers/collaborators.py +165 -0
- roadmap/adapters/github/handlers/comments.py +118 -0
- roadmap/adapters/github/handlers/issues.py +150 -0
- roadmap/adapters/github/handlers/labels.py +164 -0
- roadmap/adapters/github/handlers/milestones.py +104 -0
- roadmap/adapters/persistence/__init__.py +35 -0
- roadmap/adapters/persistence/conflict_resolver.py +190 -0
- roadmap/adapters/persistence/database_manager.py +492 -0
- roadmap/adapters/persistence/entity_sync_coordinators.py +672 -0
- roadmap/adapters/persistence/file_locking.py +348 -0
- roadmap/adapters/persistence/file_parser.py +92 -0
- roadmap/adapters/persistence/file_synchronizer.py +107 -0
- roadmap/adapters/persistence/focused_managers.py +140 -0
- roadmap/adapters/persistence/git_history.py +330 -0
- roadmap/adapters/persistence/parser/__init__.py +20 -0
- roadmap/adapters/persistence/parser/frontmatter.py +152 -0
- roadmap/adapters/persistence/parser/issue.py +245 -0
- roadmap/adapters/persistence/parser/milestone.py +146 -0
- roadmap/adapters/persistence/parser/project.py +106 -0
- roadmap/adapters/persistence/persistence.py +288 -0
- roadmap/adapters/persistence/repositories/__init__.py +15 -0
- roadmap/adapters/persistence/repositories/issue_repository.py +223 -0
- roadmap/adapters/persistence/repositories/milestone_repository.py +140 -0
- roadmap/adapters/persistence/repositories/project_repository.py +168 -0
- roadmap/adapters/persistence/repositories/remote_link_repository.py +355 -0
- roadmap/adapters/persistence/repositories/sync_state_repository.py +66 -0
- roadmap/adapters/persistence/storage/__init__.py +36 -0
- roadmap/adapters/persistence/storage/conflicts.py +101 -0
- roadmap/adapters/persistence/storage/connection_manager.py +79 -0
- roadmap/adapters/persistence/storage/issue_storage.py +89 -0
- roadmap/adapters/persistence/storage/milestone_storage.py +80 -0
- roadmap/adapters/persistence/storage/project_storage.py +102 -0
- roadmap/adapters/persistence/storage/queries.py +227 -0
- roadmap/adapters/persistence/storage/state_manager.py +652 -0
- roadmap/adapters/persistence/storage/sync_state_storage.py +207 -0
- roadmap/adapters/persistence/sync_metrics_repository.py +342 -0
- roadmap/adapters/persistence/sync_orchestrator.py +293 -0
- roadmap/adapters/persistence/sync_state_tracker.py +86 -0
- roadmap/adapters/persistence/yaml_repositories.py +711 -0
- roadmap/adapters/sync/__init__.py +21 -0
- roadmap/adapters/sync/backend_factory.py +108 -0
- roadmap/adapters/sync/backends/__init__.py +15 -0
- roadmap/adapters/sync/backends/converters.py +209 -0
- roadmap/adapters/sync/backends/github_backend_helpers.py +380 -0
- roadmap/adapters/sync/backends/github_client.py +99 -0
- roadmap/adapters/sync/backends/github_sync_backend.py +792 -0
- roadmap/adapters/sync/backends/github_sync_ops.py +908 -0
- roadmap/adapters/sync/backends/services/__init__.py +1 -0
- roadmap/adapters/sync/backends/services/github_authentication_service.py +105 -0
- roadmap/adapters/sync/backends/services/github_issue_delete_service.py +423 -0
- roadmap/adapters/sync/backends/services/github_issue_dependency_service.py +63 -0
- roadmap/adapters/sync/backends/services/github_issue_fetch_service.py +158 -0
- roadmap/adapters/sync/backends/services/github_label_sync_service.py +126 -0
- roadmap/adapters/sync/backends/services/github_local_issue_sync_service.py +229 -0
- roadmap/adapters/sync/backends/services/github_milestone_fetch_service.py +204 -0
- roadmap/adapters/sync/backends/vanilla_git_sync_backend.py +198 -0
- roadmap/adapters/sync/services/__init__.py +11 -0
- roadmap/adapters/sync/services/baseline_state_handler.py +83 -0
- roadmap/adapters/sync/services/conflict_converter.py +85 -0
- roadmap/adapters/sync/services/issue_persistence_service.py +263 -0
- roadmap/adapters/sync/services/issue_state_service.py +243 -0
- roadmap/adapters/sync/services/local_change_filter.py +129 -0
- roadmap/adapters/sync/services/pull_result_processor.py +71 -0
- roadmap/adapters/sync/services/remote_issue_creation_service.py +112 -0
- roadmap/adapters/sync/services/sync_analysis_service.py +250 -0
- roadmap/adapters/sync/services/sync_authentication_service.py +57 -0
- roadmap/adapters/sync/services/sync_data_fetch_service.py +212 -0
- roadmap/adapters/sync/services/sync_linking_service.py +329 -0
- roadmap/adapters/sync/services/sync_report_service.py +62 -0
- roadmap/adapters/sync/services/sync_state_update_service.py +51 -0
- roadmap/adapters/sync/sync_cache_orchestrator.py +454 -0
- roadmap/adapters/sync/sync_merge_engine.py +416 -0
- roadmap/adapters/sync/sync_merge_orchestrator.py +1313 -0
- roadmap/adapters/sync/sync_retrieval_orchestrator.py +810 -0
- roadmap/application/services/__init__.py +1 -0
- roadmap/application/services/deduplicate_service.py +517 -0
- roadmap/application/use_cases/__init__.py +1 -0
- roadmap/common/__init__.py +308 -0
- roadmap/common/cache.py +209 -0
- roadmap/common/cli_errors.py +147 -0
- roadmap/common/configuration/__init__.py +33 -0
- roadmap/common/configuration/config_loader.py +264 -0
- roadmap/common/configuration/config_manager.py +164 -0
- roadmap/common/configuration/config_schema.py +145 -0
- roadmap/common/configuration/github/__init__.py +9 -0
- roadmap/common/configuration/github/config_manager.py +154 -0
- roadmap/common/configuration/github/token_resolver.py +84 -0
- roadmap/common/console.py +184 -0
- roadmap/common/constants.py +134 -0
- roadmap/common/datetime_parser.py +319 -0
- roadmap/common/error_formatter.py +84 -0
- roadmap/common/errors/__init__.py +143 -0
- roadmap/common/errors/error_base.py +73 -0
- roadmap/common/errors/error_file.py +82 -0
- roadmap/common/errors/error_git.py +67 -0
- roadmap/common/errors/error_handler.py +166 -0
- roadmap/common/errors/error_network.py +90 -0
- roadmap/common/errors/error_security.py +78 -0
- roadmap/common/errors/error_standards.py +561 -0
- roadmap/common/errors/error_validation.py +111 -0
- roadmap/common/errors/exceptions.py +432 -0
- roadmap/common/formatters/__init__.py +29 -0
- roadmap/common/formatters/base_table_formatter.py +96 -0
- roadmap/common/formatters/export/__init__.py +7 -0
- roadmap/common/formatters/export/issue_exporter.py +97 -0
- roadmap/common/formatters/helpers.py +28 -0
- roadmap/common/formatters/kanban/__init__.py +9 -0
- roadmap/common/formatters/kanban/layout.py +48 -0
- roadmap/common/formatters/kanban/organizer.py +81 -0
- roadmap/common/formatters/output/__init__.py +15 -0
- roadmap/common/formatters/tables/__init__.py +11 -0
- roadmap/common/formatters/tables/column_factory.py +312 -0
- roadmap/common/formatters/tables/issue_table.py +276 -0
- roadmap/common/formatters/tables/milestone_table.py +247 -0
- roadmap/common/formatters/tables/project_table.py +195 -0
- roadmap/common/formatters/text/__init__.py +57 -0
- roadmap/common/formatters/text/basic.py +217 -0
- roadmap/common/formatters/text/display.py +42 -0
- roadmap/common/formatters/text/duration.py +36 -0
- roadmap/common/formatters/text/operations.py +325 -0
- roadmap/common/formatters/text/status_badges.py +57 -0
- roadmap/common/initialization/github/__init__.py +11 -0
- roadmap/common/initialization/github/setup_service.py +238 -0
- roadmap/common/initialization/github/setup_validator.py +78 -0
- roadmap/common/logging/__init__.py +113 -0
- roadmap/common/logging/audit_logging.py +5 -0
- roadmap/common/logging/correlation.py +45 -0
- roadmap/common/logging/decorators.py +267 -0
- roadmap/common/logging/error_logging.py +217 -0
- roadmap/common/logging/formatters.py +124 -0
- roadmap/common/logging/loggers.py +68 -0
- roadmap/common/logging/performance_tracking.py +267 -0
- roadmap/common/logging/utils.py +335 -0
- roadmap/common/models/__init__.py +44 -0
- roadmap/common/models/cli_models.py +118 -0
- roadmap/common/models/config_models.py +199 -0
- roadmap/common/models/output_models.py +381 -0
- roadmap/common/observability/__init__.py +28 -0
- roadmap/common/observability/instrumentation.py +103 -0
- roadmap/common/observability/observability.py +76 -0
- roadmap/common/observability/otel_init.py +109 -0
- roadmap/common/output_formatter.py +393 -0
- roadmap/common/progress.py +379 -0
- roadmap/common/result.py +396 -0
- roadmap/common/security/__init__.py +48 -0
- roadmap/common/security/exceptions.py +13 -0
- roadmap/common/security/export_cleanup.py +79 -0
- roadmap/common/security/file_operations.py +104 -0
- roadmap/common/security/filename_sanitization.py +54 -0
- roadmap/common/security/logging.py +27 -0
- roadmap/common/security/path_validation.py +137 -0
- roadmap/common/security/temp_files.py +41 -0
- roadmap/common/services/__init__.py +65 -0
- roadmap/common/services/decorators.py +183 -0
- roadmap/common/services/logging_utils.py +208 -0
- roadmap/common/services/metrics.py +161 -0
- roadmap/common/services/performance.py +219 -0
- roadmap/common/services/profiling.py +288 -0
- roadmap/common/services/retry.py +288 -0
- roadmap/common/status_style_manager.py +114 -0
- roadmap/common/union_find.py +88 -0
- roadmap/common/update_constants.py +13 -0
- roadmap/common/utils/__init__.py +65 -0
- roadmap/common/utils/cli_helpers.py +381 -0
- roadmap/common/utils/file_utils.py +440 -0
- roadmap/common/utils/path_utils.py +38 -0
- roadmap/common/utils/status_utils.py +130 -0
- roadmap/common/utils/timezone_utils.py +480 -0
- roadmap/common/validation/__init__.py +37 -0
- roadmap/common/validation/field_validator.py +104 -0
- roadmap/common/validation/result.py +37 -0
- roadmap/common/validation/roadmap_validator.py +310 -0
- roadmap/common/validation/schema_validator.py +36 -0
- roadmap/common/validation/validators.py +50 -0
- roadmap/core/__init__.py +1 -0
- roadmap/core/domain/__init__.py +30 -0
- roadmap/core/domain/comment.py +24 -0
- roadmap/core/domain/health.py +15 -0
- roadmap/core/domain/issue.py +274 -0
- roadmap/core/domain/milestone.py +173 -0
- roadmap/core/domain/ports/__init__.py +1 -0
- roadmap/core/domain/ports/baseline_repository.py +24 -0
- roadmap/core/domain/ports/issue_repository.py +34 -0
- roadmap/core/domain/ports/remote_backend_port.py +31 -0
- roadmap/core/domain/project.py +130 -0
- roadmap/core/interfaces/__init__.py +109 -0
- roadmap/core/interfaces/assignee_validator.py +40 -0
- roadmap/core/interfaces/backend_factory.py +175 -0
- roadmap/core/interfaces/github.py +102 -0
- roadmap/core/interfaces/parsers.py +140 -0
- roadmap/core/interfaces/persistence.py +45 -0
- roadmap/core/interfaces/repositories.py +174 -0
- roadmap/core/interfaces/state_managers.py +111 -0
- roadmap/core/interfaces/state_storage.py +64 -0
- roadmap/core/interfaces/sync_backend.py +275 -0
- roadmap/core/interfaces/sync_services.py +119 -0
- roadmap/core/interfaces/sync_validators.py +180 -0
- roadmap/core/models/__init__.py +31 -0
- roadmap/core/models/sync_models.py +162 -0
- roadmap/core/models.py +115 -0
- roadmap/core/observability/__init__.py +17 -0
- roadmap/core/observability/sync_metrics.py +699 -0
- roadmap/core/repositories.py +200 -0
- roadmap/core/services/__init__.py +313 -0
- roadmap/core/services/baseline/__init__.py +1 -0
- roadmap/core/services/baseline/baseline_builder_progress.py +274 -0
- roadmap/core/services/baseline/baseline_retriever.py +63 -0
- roadmap/core/services/baseline/baseline_selector.py +248 -0
- roadmap/core/services/baseline/baseline_state_retriever.py +284 -0
- roadmap/core/services/baseline/optimized_baseline_builder.py +425 -0
- roadmap/core/services/comment/__init__.py +1 -0
- roadmap/core/services/comment/comment_service.py +277 -0
- roadmap/core/services/git/__init__.py +1 -0
- roadmap/core/services/git/git_hook_auto_sync_service.py +403 -0
- roadmap/core/services/github/__init__.py +1 -0
- roadmap/core/services/github/github_change_detector.py +162 -0
- roadmap/core/services/github/github_config_validator.py +148 -0
- roadmap/core/services/github/github_conflict_detector.py +179 -0
- roadmap/core/services/github/github_entity_classifier.py +51 -0
- roadmap/core/services/github/github_integration_service.py +439 -0
- roadmap/core/services/github/github_issue_client.py +346 -0
- roadmap/core/services/health/__init__.py +1 -0
- roadmap/core/services/health/backup_cleanup_service.py +202 -0
- roadmap/core/services/health/data_integrity_validator_service.py +90 -0
- roadmap/core/services/health/entity_health_scanner.py +460 -0
- roadmap/core/services/health/file_repair_service.py +193 -0
- roadmap/core/services/health/health_check_service.py +166 -0
- roadmap/core/services/health/health_models.py +80 -0
- roadmap/core/services/health/infrastructure_validator_service.py +328 -0
- roadmap/core/services/health/issue_health_scanner.py +263 -0
- roadmap/core/services/helpers/__init__.py +18 -0
- roadmap/core/services/helpers/status_change_helpers.py +88 -0
- roadmap/core/services/initialization/__init__.py +19 -0
- roadmap/core/services/initialization/utils.py +104 -0
- roadmap/core/services/initialization/validator.py +78 -0
- roadmap/core/services/initialization/workflow.py +182 -0
- roadmap/core/services/initialization_service.py +111 -0
- roadmap/core/services/issue/__init__.py +1 -0
- roadmap/core/services/issue/assignee_validation_service.py +312 -0
- roadmap/core/services/issue/issue_creation_service.py +309 -0
- roadmap/core/services/issue/issue_filter_service.py +302 -0
- roadmap/core/services/issue/issue_matching_service.py +127 -0
- roadmap/core/services/issue/issue_service.py +765 -0
- roadmap/core/services/issue/issue_update_service.py +144 -0
- roadmap/core/services/issue/start_issue_service.py +174 -0
- roadmap/core/services/issue_helpers/__init__.py +18 -0
- roadmap/core/services/issue_helpers/issue_filters.py +296 -0
- roadmap/core/services/milestone_service.py +438 -0
- roadmap/core/services/project/__init__.py +1 -0
- roadmap/core/services/project/project_service.py +434 -0
- roadmap/core/services/project/project_status_service.py +125 -0
- roadmap/core/services/project_init/__init__.py +22 -0
- roadmap/core/services/project_init/context_detection.py +146 -0
- roadmap/core/services/project_init/creation.py +76 -0
- roadmap/core/services/project_init/detection.py +51 -0
- roadmap/core/services/project_init/template.py +228 -0
- roadmap/core/services/status_change_service.py +88 -0
- roadmap/core/services/sync/__init__.py +1 -0
- roadmap/core/services/sync/batch_processor.py +290 -0
- roadmap/core/services/sync/conflict_resolver.py +154 -0
- roadmap/core/services/sync/dependency_resolver.py +367 -0
- roadmap/core/services/sync/duplicate_detector.py +771 -0
- roadmap/core/services/sync/duplicate_resolver.py +315 -0
- roadmap/core/services/sync/error_classification.py +470 -0
- roadmap/core/services/sync/performance.py +263 -0
- roadmap/core/services/sync/sync_change_computer.py +211 -0
- roadmap/core/services/sync/sync_checkpoint.py +348 -0
- roadmap/core/services/sync/sync_conflict_detector.py +147 -0
- roadmap/core/services/sync/sync_conflict_resolver.py +448 -0
- roadmap/core/services/sync/sync_errors.py +383 -0
- roadmap/core/services/sync/sync_key_normalizer.py +222 -0
- roadmap/core/services/sync/sync_metadata_service.py +361 -0
- roadmap/core/services/sync/sync_plan.py +171 -0
- roadmap/core/services/sync/sync_plan_executor.py +447 -0
- roadmap/core/services/sync/sync_report.py +296 -0
- roadmap/core/services/sync/sync_state.py +173 -0
- roadmap/core/services/sync/sync_state_comparator.py +690 -0
- roadmap/core/services/sync/sync_state_manager.py +431 -0
- roadmap/core/services/sync/sync_state_normalizer.py +98 -0
- roadmap/core/services/sync/sync_three_way.py +78 -0
- roadmap/core/services/sync/three_way_merger.py +178 -0
- roadmap/core/services/utils/__init__.py +1 -0
- roadmap/core/services/utils/configuration_service.py +113 -0
- roadmap/core/services/utils/critical_path_calculator.py +418 -0
- roadmap/core/services/utils/dependency_analyzer.py +360 -0
- roadmap/core/services/utils/field_conflict_detector.py +126 -0
- roadmap/core/services/utils/remote_fetcher.py +202 -0
- roadmap/core/services/utils/remote_state_normalizer.py +110 -0
- roadmap/core/services/utils/retry_policy.py +51 -0
- roadmap/core/services/validator_base.py +130 -0
- roadmap/core/services/validators/__init__.py +28 -0
- roadmap/core/services/validators/_utils.py +20 -0
- roadmap/core/services/validators/archivable_issues_validator.py +71 -0
- roadmap/core/services/validators/archivable_milestones_validator.py +69 -0
- roadmap/core/services/validators/backup_validator.py +89 -0
- roadmap/core/services/validators/data_integrity_validator.py +84 -0
- roadmap/core/services/validators/duplicate_issues_validator.py +75 -0
- roadmap/core/services/validators/duplicate_milestones_validator.py +141 -0
- roadmap/core/services/validators/folder_structure_validator.py +165 -0
- roadmap/core/services/validators/health_status_utils.py +28 -0
- roadmap/core/services/validators/milestone_naming_validator.py +146 -0
- roadmap/core/services/validators/missing_headlines_validator.py +97 -0
- roadmap/core/services/validators/orphaned_issues_validator.py +142 -0
- roadmap/core/services/validators/orphaned_milestones_validator.py +85 -0
- roadmap/core/utils/git_remote_parser.py +83 -0
- roadmap/core/utils/github_urls.py +66 -0
- roadmap/domain/validation.py +30 -0
- roadmap/infrastructure/__init__.py +55 -0
- roadmap/infrastructure/coordination/__init__.py +8 -0
- roadmap/infrastructure/coordination/coordinator_params.py +82 -0
- roadmap/infrastructure/coordination/core.py +446 -0
- roadmap/infrastructure/coordination/git_coordinator.py +160 -0
- roadmap/infrastructure/coordination/initialization.py +348 -0
- roadmap/infrastructure/coordination/issue_coordinator.py +154 -0
- roadmap/infrastructure/coordination/issue_operations.py +456 -0
- roadmap/infrastructure/coordination/milestone_coordinator.py +135 -0
- roadmap/infrastructure/coordination/milestone_operations.py +201 -0
- roadmap/infrastructure/coordination/project_coordinator.py +113 -0
- roadmap/infrastructure/coordination/project_operations.py +144 -0
- roadmap/infrastructure/coordination/team_coordinator.py +107 -0
- roadmap/infrastructure/coordination/user_operations.py +150 -0
- roadmap/infrastructure/coordination/validation_coordinator.py +249 -0
- roadmap/infrastructure/coordination_gateway.py +152 -0
- roadmap/infrastructure/git/__init__.py +6 -0
- roadmap/infrastructure/git/git_integration_ops.py +234 -0
- roadmap/infrastructure/git_gateway.py +35 -0
- roadmap/infrastructure/github_gateway.py +52 -0
- roadmap/infrastructure/maintenance/__init__.py +5 -0
- roadmap/infrastructure/maintenance/cleanup.py +483 -0
- roadmap/infrastructure/observability/__init__.py +7 -0
- roadmap/infrastructure/observability/health.py +196 -0
- roadmap/infrastructure/observability/health_formatter.py +73 -0
- roadmap/infrastructure/observability/specialized_health_checkers.py +188 -0
- roadmap/infrastructure/persistence_gateway.py +164 -0
- roadmap/infrastructure/security/__init__.py +1 -0
- roadmap/infrastructure/security/credentials.py +489 -0
- roadmap/infrastructure/sync_gateway.py +94 -0
- roadmap/infrastructure/validation/__init__.py +7 -0
- roadmap/infrastructure/validation/file_enumeration.py +219 -0
- roadmap/infrastructure/validation/github_assignee_validator.py +47 -0
- roadmap/infrastructure/validation/github_validator.py +101 -0
- roadmap/infrastructure/validation/milestone_consistency_validator.py +96 -0
- roadmap/infrastructure/validation/vanilla_assignee_validator.py +37 -0
- roadmap/infrastructure/validation_gateway.py +96 -0
- roadmap/presentation/formatters/sync_metrics_formatter.py +355 -0
- roadmap/settings.py +345 -0
- roadmap/templates/github_workflows/ci-cd-roadmap.yml +241 -0
- roadmap/templates/github_workflows/issue-lifecycle.yml +236 -0
- roadmap/templates/github_workflows/roadmap-integration.yml +137 -0
- roadmap/templates/github_workflows/roadmap-starter.yml +59 -0
- roadmap/version.py +381 -0
- roadmap_cli-0.1.1.dist-info/METADATA +448 -0
- roadmap_cli-0.1.1.dist-info/RECORD +521 -0
- roadmap_cli-0.1.1.dist-info/WHEEL +4 -0
- roadmap_cli-0.1.1.dist-info/entry_points.txt +2 -0
- roadmap_cli-0.1.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Enhanced dry-run display for sync operations."""
|
|
2
|
+
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
from roadmap.common.console import get_console
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _display_active_filters(
|
|
9
|
+
console,
|
|
10
|
+
milestone_filter: tuple[str, ...] | None,
|
|
11
|
+
milestone_state: str | None,
|
|
12
|
+
since: str | None,
|
|
13
|
+
until: str | None,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Display active filters for the sync operation."""
|
|
16
|
+
if not (milestone_filter or milestone_state != "all" or since or until):
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
console.print("[bold yellow]Active Filters:[/bold yellow]")
|
|
20
|
+
if milestone_filter:
|
|
21
|
+
console.print(f" • Milestones: {', '.join(milestone_filter)}", style="yellow")
|
|
22
|
+
if milestone_state and milestone_state != "all":
|
|
23
|
+
console.print(f" • State: {milestone_state}", style="yellow")
|
|
24
|
+
if since:
|
|
25
|
+
console.print(f" • Since: {since}", style="yellow")
|
|
26
|
+
if until:
|
|
27
|
+
console.print(f" • Until: {until}", style="yellow")
|
|
28
|
+
console.print()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _display_summary_statistics(console, push_changes, pull_changes, conflicts) -> None:
|
|
32
|
+
"""Display summary statistics of changes."""
|
|
33
|
+
console.print("[bold]Summary:[/bold]")
|
|
34
|
+
console.print(f" • Issues to push: [cyan]{len(push_changes)}[/cyan]")
|
|
35
|
+
console.print(f" • Issues to pull: [green]{len(pull_changes)}[/green]")
|
|
36
|
+
console.print(f" • Conflicts detected: [red]{len(conflicts)}[/red]")
|
|
37
|
+
console.print()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def display_detailed_dry_run_preview(
|
|
41
|
+
analysis_report,
|
|
42
|
+
milestone_filter: tuple[str, ...] | None = None,
|
|
43
|
+
milestone_state: str | None = None,
|
|
44
|
+
since: str | None = None,
|
|
45
|
+
until: str | None = None,
|
|
46
|
+
verbose: bool = False,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Display detailed preview of sync operations in dry-run mode.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
analysis_report: Sync analysis report with detected changes
|
|
52
|
+
milestone_filter: Tuple of milestone names to filter by
|
|
53
|
+
milestone_state: Filter milestones by state (open/closed/all)
|
|
54
|
+
since: Filter milestones updated since this date
|
|
55
|
+
until: Filter milestones updated until this date
|
|
56
|
+
verbose: Show detailed information
|
|
57
|
+
"""
|
|
58
|
+
console = get_console()
|
|
59
|
+
|
|
60
|
+
# Header
|
|
61
|
+
console.print("\n[bold cyan]═══ DRY-RUN PREVIEW ═══[/bold cyan]")
|
|
62
|
+
console.print(
|
|
63
|
+
"[dim]The following changes would be applied if you run sync without --dry-run:[/dim]\n"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Show active filters
|
|
67
|
+
_display_active_filters(console, milestone_filter, milestone_state, since, until)
|
|
68
|
+
|
|
69
|
+
# Get changes by type
|
|
70
|
+
if not hasattr(analysis_report, "changes") or not analysis_report.changes:
|
|
71
|
+
console.print("[dim]No changes detected[/dim]")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
changes = analysis_report.changes
|
|
75
|
+
push_changes = [c for c in changes if c.is_local_only_change()]
|
|
76
|
+
pull_changes = [c for c in changes if c.is_remote_only_change()]
|
|
77
|
+
conflicts = [c for c in changes if c.has_conflict]
|
|
78
|
+
|
|
79
|
+
# Summary statistics
|
|
80
|
+
_display_summary_statistics(console, push_changes, pull_changes, conflicts)
|
|
81
|
+
|
|
82
|
+
# Issues to push
|
|
83
|
+
if push_changes:
|
|
84
|
+
_display_push_operations(push_changes, console, verbose)
|
|
85
|
+
|
|
86
|
+
# Issues to pull
|
|
87
|
+
if pull_changes:
|
|
88
|
+
_display_pull_operations(pull_changes, console, verbose)
|
|
89
|
+
|
|
90
|
+
# Conflicts
|
|
91
|
+
if conflicts:
|
|
92
|
+
_display_conflicts(conflicts, console, verbose)
|
|
93
|
+
|
|
94
|
+
# Footer
|
|
95
|
+
console.print(
|
|
96
|
+
"\n[bold yellow]⚠️ No changes will be applied in dry-run mode[/bold yellow]"
|
|
97
|
+
)
|
|
98
|
+
console.print(
|
|
99
|
+
"[dim]To apply these changes, run: roadmap sync (without --dry-run)[/dim]"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _display_push_operations(push_changes, console, verbose: bool) -> None:
|
|
104
|
+
"""Display issues that would be pushed to remote."""
|
|
105
|
+
console.print("[bold cyan]📤 PUSH Operations (Local → Remote):[/bold cyan]")
|
|
106
|
+
|
|
107
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
108
|
+
table.add_column("Issue ID", style="dim", width=12)
|
|
109
|
+
table.add_column("Title", width=40)
|
|
110
|
+
table.add_column("Action", width=15)
|
|
111
|
+
|
|
112
|
+
if verbose:
|
|
113
|
+
table.add_column("Changes", width=30)
|
|
114
|
+
|
|
115
|
+
for change in push_changes[:20]: # Limit to first 20
|
|
116
|
+
action = _determine_push_action(change)
|
|
117
|
+
table.add_row(
|
|
118
|
+
change.issue_id[:10],
|
|
119
|
+
change.title[:37] + "..." if len(change.title) > 40 else change.title,
|
|
120
|
+
action,
|
|
121
|
+
_format_changes(change) if verbose else "",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
console.print(table)
|
|
125
|
+
|
|
126
|
+
if len(push_changes) > 20:
|
|
127
|
+
console.print(
|
|
128
|
+
f"[dim] ... and {len(push_changes) - 20} more[/dim]",
|
|
129
|
+
style="cyan",
|
|
130
|
+
)
|
|
131
|
+
console.print()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _display_pull_operations(pull_changes, console, verbose: bool) -> None:
|
|
135
|
+
"""Display issues that would be pulled from remote."""
|
|
136
|
+
console.print("[bold green]📥 PULL Operations (Remote → Local):[/bold green]")
|
|
137
|
+
|
|
138
|
+
table = Table(show_header=True, header_style="bold green")
|
|
139
|
+
table.add_column("Issue ID", style="dim", width=12)
|
|
140
|
+
table.add_column("Title", width=40)
|
|
141
|
+
table.add_column("Action", width=15)
|
|
142
|
+
|
|
143
|
+
if verbose:
|
|
144
|
+
table.add_column("Changes", width=30)
|
|
145
|
+
|
|
146
|
+
for change in pull_changes[:20]: # Limit to first 20
|
|
147
|
+
action = _determine_pull_action(change)
|
|
148
|
+
table.add_row(
|
|
149
|
+
change.issue_id[:10],
|
|
150
|
+
change.title[:37] + "..." if len(change.title) > 40 else change.title,
|
|
151
|
+
action,
|
|
152
|
+
_format_changes(change) if verbose else "",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
console.print(table)
|
|
156
|
+
|
|
157
|
+
if len(pull_changes) > 20:
|
|
158
|
+
console.print(
|
|
159
|
+
f"[dim] ... and {len(pull_changes) - 20} more[/dim]",
|
|
160
|
+
style="green",
|
|
161
|
+
)
|
|
162
|
+
console.print()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _display_conflicts(conflicts, console, verbose: bool) -> None:
|
|
166
|
+
"""Display detected conflicts."""
|
|
167
|
+
console.print("[bold red]⚠️ CONFLICTS Detected:[/bold red]")
|
|
168
|
+
console.print(
|
|
169
|
+
"[dim]These issues have conflicting changes between local and remote:[/dim]\n"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
table = Table(show_header=True, header_style="bold red")
|
|
173
|
+
table.add_column("Issue ID", style="dim", width=12)
|
|
174
|
+
table.add_column("Title", width=40)
|
|
175
|
+
table.add_column("Conflict Fields", width=25, style="yellow")
|
|
176
|
+
|
|
177
|
+
for conflict in conflicts[:10]: # Limit to first 10
|
|
178
|
+
conflict_fields = _get_conflict_fields(conflict)
|
|
179
|
+
table.add_row(
|
|
180
|
+
conflict.issue_id[:10],
|
|
181
|
+
conflict.title[:37] + "..." if len(conflict.title) > 40 else conflict.title,
|
|
182
|
+
conflict_fields,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
console.print(table)
|
|
186
|
+
|
|
187
|
+
if len(conflicts) > 10:
|
|
188
|
+
console.print(f"[dim] ... and {len(conflicts) - 10} more[/dim]", style="red")
|
|
189
|
+
|
|
190
|
+
console.print(
|
|
191
|
+
"\n[yellow]💡 Tip: Use --force-local or --force-remote to auto-resolve conflicts[/yellow]"
|
|
192
|
+
)
|
|
193
|
+
console.print()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _determine_push_action(change) -> str:
|
|
197
|
+
"""Determine the push action type from a change object."""
|
|
198
|
+
if change.is_local_only_change():
|
|
199
|
+
return "[cyan]CREATE[/cyan]"
|
|
200
|
+
elif hasattr(change, "field_changes") and change.field_changes:
|
|
201
|
+
return "[yellow]UPDATE[/yellow]"
|
|
202
|
+
return "[green]SYNC[/green]"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _determine_pull_action(change) -> str:
|
|
206
|
+
"""Determine the pull action type from a change object."""
|
|
207
|
+
if change.is_remote_only_change():
|
|
208
|
+
return "[green]CREATE[/green]"
|
|
209
|
+
elif hasattr(change, "field_changes") and change.field_changes:
|
|
210
|
+
return "[yellow]UPDATE[/yellow]"
|
|
211
|
+
return "[cyan]SYNC[/cyan]"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _format_changes(change) -> str:
|
|
215
|
+
"""Format field changes for display."""
|
|
216
|
+
if not hasattr(change, "field_changes") or not change.field_changes:
|
|
217
|
+
return "[dim]new[/dim]"
|
|
218
|
+
|
|
219
|
+
fields = list(change.field_changes.keys())[:3] # First 3 fields
|
|
220
|
+
if len(fields) == 0:
|
|
221
|
+
return "[dim]metadata[/dim]"
|
|
222
|
+
|
|
223
|
+
result = ", ".join(fields)
|
|
224
|
+
if len(change.field_changes) > 3:
|
|
225
|
+
result += f", +{len(change.field_changes) - 3} more"
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _get_conflict_fields(conflict) -> str:
|
|
230
|
+
"""Get the conflicting fields from a conflict object."""
|
|
231
|
+
if hasattr(conflict, "conflicting_fields") and conflict.conflicting_fields:
|
|
232
|
+
fields = list(conflict.conflicting_fields)[:3]
|
|
233
|
+
result = ", ".join(fields)
|
|
234
|
+
if len(conflict.conflicting_fields) > 3:
|
|
235
|
+
result += "..."
|
|
236
|
+
return result
|
|
237
|
+
return "[dim]unknown[/dim]"
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Interactive conflict resolution with rich UI for user choices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from structlog import get_logger
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from roadmap.core.domain.issue import Issue
|
|
17
|
+
from roadmap.core.services.sync.sync_conflict_resolver import Conflict
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InteractiveConflictResolver:
|
|
23
|
+
"""Provides interactive UI for resolving sync conflicts."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, console: Console | None = None):
|
|
26
|
+
"""Initialize interactive conflict resolver.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
console: Rich console for output (creates new one if None)
|
|
30
|
+
"""
|
|
31
|
+
self.console = console or Console()
|
|
32
|
+
|
|
33
|
+
def resolve_interactively(
|
|
34
|
+
self,
|
|
35
|
+
conflicts: list[Conflict],
|
|
36
|
+
issues_by_id: dict[str, Issue],
|
|
37
|
+
) -> dict[str, dict[str, str]]:
|
|
38
|
+
"""Resolve conflicts interactively with user input.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
conflicts: List of conflicts to resolve
|
|
42
|
+
issues_by_id: Dictionary mapping issue IDs to Issue objects
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dictionary of resolutions: {issue_id: {field: resolution_strategy}}
|
|
46
|
+
"""
|
|
47
|
+
if not conflicts:
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
self.console.print(
|
|
51
|
+
"\n[bold cyan]═══ Interactive Conflict Resolution ═══[/bold cyan]\n"
|
|
52
|
+
)
|
|
53
|
+
self.console.print(
|
|
54
|
+
f"Found [yellow]{len(conflicts)}[/yellow] conflicts that need your attention.\n"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
resolutions = {}
|
|
58
|
+
|
|
59
|
+
for i, conflict in enumerate(conflicts, 1):
|
|
60
|
+
self.console.print(
|
|
61
|
+
f"[bold]Conflict {i}/{len(conflicts)}[/bold]",
|
|
62
|
+
style="cyan",
|
|
63
|
+
)
|
|
64
|
+
self.console.print()
|
|
65
|
+
|
|
66
|
+
issue = issues_by_id.get(conflict.issue_id)
|
|
67
|
+
if not issue:
|
|
68
|
+
logger.warning(
|
|
69
|
+
"conflict_issue_not_found",
|
|
70
|
+
issue_id=conflict.issue_id,
|
|
71
|
+
action="resolve_conflict",
|
|
72
|
+
)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# Display conflict context
|
|
76
|
+
self._display_conflict_context(conflict, issue)
|
|
77
|
+
|
|
78
|
+
# Resolve each conflicting field
|
|
79
|
+
issue_resolutions = {}
|
|
80
|
+
for field in conflict.field_names:
|
|
81
|
+
resolution = self._resolve_field_conflict(conflict, field)
|
|
82
|
+
issue_resolutions[field] = resolution
|
|
83
|
+
|
|
84
|
+
resolutions[conflict.issue_id] = issue_resolutions
|
|
85
|
+
|
|
86
|
+
# Show confirmation
|
|
87
|
+
if i < len(conflicts):
|
|
88
|
+
if not Confirm.ask(
|
|
89
|
+
"\n[dim]Continue to next conflict?[/dim]", default=True
|
|
90
|
+
):
|
|
91
|
+
self.console.print(
|
|
92
|
+
"[yellow]Remaining conflicts will use default strategy[/yellow]"
|
|
93
|
+
)
|
|
94
|
+
break
|
|
95
|
+
self.console.print("\n" + "─" * 60 + "\n")
|
|
96
|
+
|
|
97
|
+
return resolutions
|
|
98
|
+
|
|
99
|
+
def _display_conflict_context(self, conflict: Conflict, issue: Issue) -> None:
|
|
100
|
+
"""Display context information about the conflict."""
|
|
101
|
+
# Issue header
|
|
102
|
+
header_text = Text()
|
|
103
|
+
header_text.append(f"#{issue.id}", style="bold cyan")
|
|
104
|
+
header_text.append(" • ", style="dim")
|
|
105
|
+
header_text.append(issue.title, style="bold white")
|
|
106
|
+
|
|
107
|
+
self.console.print(Panel(header_text, border_style="cyan", padding=(0, 2)))
|
|
108
|
+
self.console.print()
|
|
109
|
+
|
|
110
|
+
# Conflict summary
|
|
111
|
+
summary_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
112
|
+
summary_table.add_column(style="dim", width=20)
|
|
113
|
+
summary_table.add_column()
|
|
114
|
+
|
|
115
|
+
summary_table.add_row("Conflicting Fields:", ", ".join(conflict.field_names))
|
|
116
|
+
|
|
117
|
+
if conflict.local_updated and conflict.remote_updated:
|
|
118
|
+
summary_table.add_row(
|
|
119
|
+
"Last Updated:",
|
|
120
|
+
f"Local: {conflict.local_updated.isoformat()[:10]} | Remote: {conflict.remote_updated.isoformat()[:10]}",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self.console.print(summary_table)
|
|
124
|
+
self.console.print()
|
|
125
|
+
|
|
126
|
+
def _resolve_field_conflict(self, conflict: Conflict, field: str) -> str:
|
|
127
|
+
"""Resolve a single field conflict interactively.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
conflict: Conflict object
|
|
131
|
+
field: Field name to resolve
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Resolution strategy chosen by user
|
|
135
|
+
"""
|
|
136
|
+
# Get values from the conflict's local and remote issues
|
|
137
|
+
local_value = getattr(conflict.local_issue, field, "[dim]<none>[/dim]")
|
|
138
|
+
remote_value = conflict.remote_issue.get(field, "[dim]<none>[/dim]")
|
|
139
|
+
base_value = None # Base value not currently stored in Conflict
|
|
140
|
+
|
|
141
|
+
self.console.print(f"[bold yellow]Field:[/bold yellow] [cyan]{field}[/cyan]")
|
|
142
|
+
self.console.print()
|
|
143
|
+
|
|
144
|
+
# Show values side-by-side
|
|
145
|
+
comparison_table = Table(show_header=True, box=None, padding=(0, 2))
|
|
146
|
+
comparison_table.add_column("Version", style="bold", width=12)
|
|
147
|
+
comparison_table.add_column("Value", width=50)
|
|
148
|
+
|
|
149
|
+
if base_value is not None:
|
|
150
|
+
comparison_table.add_row(
|
|
151
|
+
"[dim]Baseline[/dim]",
|
|
152
|
+
self._format_value_for_display(base_value, field),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
comparison_table.add_row(
|
|
156
|
+
"[blue]Local[/blue]",
|
|
157
|
+
self._format_value_for_display(local_value, field),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
comparison_table.add_row(
|
|
161
|
+
"[magenta]Remote[/magenta]",
|
|
162
|
+
self._format_value_for_display(remote_value, field),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.console.print(comparison_table)
|
|
166
|
+
self.console.print()
|
|
167
|
+
|
|
168
|
+
# Present resolution options
|
|
169
|
+
options = {
|
|
170
|
+
"1": ("LOCAL", "Keep local version (blue)"),
|
|
171
|
+
"2": ("REMOTE", "Keep remote version (magenta)"),
|
|
172
|
+
"3": ("VIEW_DIFF", "View detailed diff"),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Add merge option for mergeable fields
|
|
176
|
+
if self._is_mergeable_field(field, local_value, remote_value):
|
|
177
|
+
options["4"] = ("MERGE", "Attempt automatic merge")
|
|
178
|
+
options["5"] = ("MANUAL", "Edit manually")
|
|
179
|
+
else:
|
|
180
|
+
options["4"] = ("MANUAL", "Edit manually")
|
|
181
|
+
|
|
182
|
+
options["s"] = ("SKIP", "Skip (keep flagged for review)")
|
|
183
|
+
|
|
184
|
+
# Display options
|
|
185
|
+
self.console.print("[bold]Choose resolution:[/bold]")
|
|
186
|
+
for key, (_strategy, description) in options.items():
|
|
187
|
+
self.console.print(f" {key}. {description}")
|
|
188
|
+
self.console.print()
|
|
189
|
+
|
|
190
|
+
# Get user choice
|
|
191
|
+
while True:
|
|
192
|
+
choice = Prompt.ask(
|
|
193
|
+
"Your choice",
|
|
194
|
+
choices=list(options.keys()),
|
|
195
|
+
default="1",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
strategy = options[choice][0]
|
|
199
|
+
|
|
200
|
+
if strategy == "VIEW_DIFF":
|
|
201
|
+
self._show_detailed_diff(field, local_value, remote_value, base_value)
|
|
202
|
+
continue # Ask again after showing diff
|
|
203
|
+
|
|
204
|
+
if strategy == "MANUAL":
|
|
205
|
+
manual_value = self._get_manual_edit(field, local_value, remote_value)
|
|
206
|
+
if manual_value is not None:
|
|
207
|
+
# Store manual value for later application
|
|
208
|
+
return f"MANUAL:{manual_value}"
|
|
209
|
+
continue # Ask again if manual edit cancelled
|
|
210
|
+
|
|
211
|
+
return strategy
|
|
212
|
+
|
|
213
|
+
def _format_value_for_display(self, value: Any, field: str) -> str:
|
|
214
|
+
"""Format a value for display in comparison table.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
value: Value to format
|
|
218
|
+
field: Field name (for context)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Formatted string for display
|
|
222
|
+
"""
|
|
223
|
+
if value is None or value == "[dim]<none>[/dim]":
|
|
224
|
+
return "[dim]<none>[/dim]"
|
|
225
|
+
|
|
226
|
+
# Truncate long values
|
|
227
|
+
value_str = str(value)
|
|
228
|
+
if len(value_str) > 47:
|
|
229
|
+
return value_str[:44] + "..."
|
|
230
|
+
|
|
231
|
+
return value_str
|
|
232
|
+
|
|
233
|
+
def _is_mergeable_field(
|
|
234
|
+
self, field: str, local_value: Any, remote_value: Any
|
|
235
|
+
) -> bool:
|
|
236
|
+
"""Check if a field can be automatically merged.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
field: Field name
|
|
240
|
+
local_value: Local value
|
|
241
|
+
remote_value: Remote value
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if field is mergeable
|
|
245
|
+
"""
|
|
246
|
+
# Lists can often be merged
|
|
247
|
+
if isinstance(local_value, list) and isinstance(remote_value, list):
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
# Tags, labels, etc. can be merged
|
|
251
|
+
mergeable_fields = {"tags", "labels", "assignees", "dependencies"}
|
|
252
|
+
if field in mergeable_fields:
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def _show_detailed_diff(
|
|
258
|
+
self,
|
|
259
|
+
field: str,
|
|
260
|
+
local_value: Any,
|
|
261
|
+
remote_value: Any,
|
|
262
|
+
base_value: Any,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Show detailed diff of values.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
field: Field name
|
|
268
|
+
local_value: Local value
|
|
269
|
+
remote_value: Remote value
|
|
270
|
+
base_value: Baseline value
|
|
271
|
+
"""
|
|
272
|
+
self.console.print("\n[bold]Detailed Diff:[/bold]\n")
|
|
273
|
+
|
|
274
|
+
# Format values for syntax highlighting
|
|
275
|
+
local_formatted = self._format_value_for_diff(local_value)
|
|
276
|
+
remote_formatted = self._format_value_for_diff(remote_value)
|
|
277
|
+
|
|
278
|
+
# Display side-by-side if possible
|
|
279
|
+
diff_table = Table.grid(padding=(0, 2))
|
|
280
|
+
diff_table.add_column("Local", style="blue", width=40)
|
|
281
|
+
diff_table.add_column("Remote", style="magenta", width=40)
|
|
282
|
+
|
|
283
|
+
if base_value is not None:
|
|
284
|
+
base_formatted = self._format_value_for_diff(base_value)
|
|
285
|
+
self.console.print(
|
|
286
|
+
Panel(
|
|
287
|
+
Syntax(base_formatted, "yaml", theme="monokai", line_numbers=False),
|
|
288
|
+
title="[dim]Baseline[/dim]",
|
|
289
|
+
border_style="dim",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
self.console.print()
|
|
293
|
+
|
|
294
|
+
# Show local and remote side by side
|
|
295
|
+
self.console.print("[bold blue]Local:[/bold blue]")
|
|
296
|
+
self.console.print(
|
|
297
|
+
Syntax(local_formatted, "yaml", theme="monokai", line_numbers=False)
|
|
298
|
+
)
|
|
299
|
+
self.console.print()
|
|
300
|
+
|
|
301
|
+
self.console.print("[bold magenta]Remote:[/bold magenta]")
|
|
302
|
+
self.console.print(
|
|
303
|
+
Syntax(remote_formatted, "yaml", theme="monokai", line_numbers=False)
|
|
304
|
+
)
|
|
305
|
+
self.console.print()
|
|
306
|
+
|
|
307
|
+
def _format_value_for_diff(self, value: Any) -> str:
|
|
308
|
+
"""Format value for syntax-highlighted diff display."""
|
|
309
|
+
if isinstance(value, (list, dict)):
|
|
310
|
+
import json
|
|
311
|
+
|
|
312
|
+
return json.dumps(value, indent=2)
|
|
313
|
+
return str(value)
|
|
314
|
+
|
|
315
|
+
def _get_manual_edit(
|
|
316
|
+
self,
|
|
317
|
+
field: str,
|
|
318
|
+
local_value: Any,
|
|
319
|
+
remote_value: Any,
|
|
320
|
+
) -> str | None:
|
|
321
|
+
"""Get manual edit from user.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
field: Field name
|
|
325
|
+
local_value: Local value
|
|
326
|
+
remote_value: Remote value
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Manually edited value or None if cancelled
|
|
330
|
+
"""
|
|
331
|
+
self.console.print("\n[bold]Manual Edit:[/bold]")
|
|
332
|
+
self.console.print(
|
|
333
|
+
"[dim]Enter the value you want to use, or press Ctrl+C to cancel[/dim]\n"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
self.console.print(f"Current local: {local_value}")
|
|
337
|
+
self.console.print(f"Current remote: {remote_value}")
|
|
338
|
+
self.console.print()
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
manual_value = Prompt.ask(f"Enter value for '{field}'")
|
|
342
|
+
if manual_value:
|
|
343
|
+
return manual_value
|
|
344
|
+
return None
|
|
345
|
+
except KeyboardInterrupt:
|
|
346
|
+
self.console.print("\n[yellow]Manual edit cancelled[/yellow]")
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def show_conflict_summary(
|
|
350
|
+
self,
|
|
351
|
+
conflicts: list[Conflict],
|
|
352
|
+
resolutions: dict[str, dict[str, str]],
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Show summary of conflict resolutions.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
conflicts: Original list of conflicts
|
|
358
|
+
resolutions: Resolutions chosen by user
|
|
359
|
+
"""
|
|
360
|
+
self.console.print(
|
|
361
|
+
"\n[bold cyan]═══ Conflict Resolution Summary ═══[/bold cyan]\n"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
summary_table = Table(show_header=True)
|
|
365
|
+
summary_table.add_column("Issue ID", style="cyan", width=15)
|
|
366
|
+
summary_table.add_column("Field", width=20)
|
|
367
|
+
summary_table.add_column("Resolution", width=20)
|
|
368
|
+
|
|
369
|
+
for conflict in conflicts:
|
|
370
|
+
issue_resolutions = resolutions.get(conflict.issue_id, {})
|
|
371
|
+
for field in conflict.field_names:
|
|
372
|
+
resolution = issue_resolutions.get(field, "SKIP")
|
|
373
|
+
resolution_display = resolution
|
|
374
|
+
if resolution.startswith("MANUAL:"):
|
|
375
|
+
resolution_display = "MANUAL"
|
|
376
|
+
|
|
377
|
+
summary_table.add_row(
|
|
378
|
+
conflict.issue_id[:12],
|
|
379
|
+
field,
|
|
380
|
+
resolution_display,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
self.console.print(summary_table)
|
|
384
|
+
self.console.print()
|
|
385
|
+
|
|
386
|
+
# Show statistics
|
|
387
|
+
total_fields = sum(len(c.field_names) for c in conflicts)
|
|
388
|
+
resolved_fields = sum(
|
|
389
|
+
len([r for r in resolutions.get(c.issue_id, {}).values() if r != "SKIP"])
|
|
390
|
+
for c in conflicts
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
self.console.print(
|
|
394
|
+
f"[green]Resolved:[/green] {resolved_fields}/{total_fields} fields"
|
|
395
|
+
)
|
|
396
|
+
self.console.print()
|