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,1117 @@
|
|
|
1
|
+
"""Top-level sync command - backend-agnostic interface to sync operations.
|
|
2
|
+
|
|
3
|
+
This command provides a unified interface for syncing with any backend
|
|
4
|
+
(GitHub, Git, etc.) without requiring the user to specify which backend.
|
|
5
|
+
The backend is automatically detected from configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from structlog import get_logger
|
|
12
|
+
|
|
13
|
+
from roadmap.adapters.cli.cli_command_helpers import require_initialized
|
|
14
|
+
from roadmap.adapters.cli.sync_metrics_command import sync_metrics
|
|
15
|
+
from roadmap.common.console import get_console
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _show_baseline(core, backend, verbose, console_inst) -> bool:
|
|
21
|
+
"""Handle the `--base` flag: show or create baseline state."""
|
|
22
|
+
from roadmap.adapters.cli.sync_handlers import show_baseline
|
|
23
|
+
|
|
24
|
+
return show_baseline(core, backend, verbose, console_inst)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _reset_baseline(core, backend, verbose, console_inst) -> bool:
|
|
28
|
+
"""Handle the `--reset-baseline` flag: force recalculation of baseline."""
|
|
29
|
+
from roadmap.adapters.cli.services.sync_service import get_sync_backend
|
|
30
|
+
from roadmap.adapters.cli.sync_context import (
|
|
31
|
+
_clear_baseline_db,
|
|
32
|
+
_create_and_save_baseline,
|
|
33
|
+
_resolve_backend_and_init,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
console_inst.print(
|
|
37
|
+
"⚠️ WARNING: Resetting baseline will:",
|
|
38
|
+
style="bold yellow",
|
|
39
|
+
)
|
|
40
|
+
console_inst.print(" • Clear all sync history")
|
|
41
|
+
console_inst.print(" • Treat all current issues as the new baseline")
|
|
42
|
+
console_inst.print(" • Next sync will see them as baseline (no changes)")
|
|
43
|
+
console_inst.print()
|
|
44
|
+
|
|
45
|
+
if not click.confirm("Continue with baseline reset?"):
|
|
46
|
+
console_inst.print("Cancelled.", style="dim")
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# Determine backend
|
|
50
|
+
backend_type, sync_backend = _resolve_backend_and_init(
|
|
51
|
+
core, backend, get_sync_backend
|
|
52
|
+
)
|
|
53
|
+
if not sync_backend:
|
|
54
|
+
console_inst.print("❌ Failed to initialize backend", style="bold red")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
# Clear existing baseline from database
|
|
58
|
+
_clear_baseline_db(core, console_inst)
|
|
59
|
+
|
|
60
|
+
# Create fresh baseline from current local state and save
|
|
61
|
+
success = _create_and_save_baseline(
|
|
62
|
+
core, sync_backend, backend_type, console_inst, verbose
|
|
63
|
+
)
|
|
64
|
+
if not success:
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _init_sync_context(
|
|
71
|
+
core,
|
|
72
|
+
backend,
|
|
73
|
+
baseline_option,
|
|
74
|
+
dry_run,
|
|
75
|
+
verbose,
|
|
76
|
+
console_inst,
|
|
77
|
+
enable_duplicate_detection=True,
|
|
78
|
+
title_threshold=0.90,
|
|
79
|
+
content_threshold=0.85,
|
|
80
|
+
auto_resolve_threshold=0.95,
|
|
81
|
+
):
|
|
82
|
+
"""Initialize backend, orchestrator, and required services for sync command.
|
|
83
|
+
|
|
84
|
+
Returns tuple: (backend_type, sync_backend, orchestrator, pre_sync_baseline, pre_sync_issue_count, state_comparator, conflict_resolver)
|
|
85
|
+
"""
|
|
86
|
+
from roadmap.adapters.cli.sync_context import _init_sync_context as _ctx
|
|
87
|
+
|
|
88
|
+
return _ctx(
|
|
89
|
+
core,
|
|
90
|
+
backend,
|
|
91
|
+
baseline_option,
|
|
92
|
+
dry_run,
|
|
93
|
+
verbose,
|
|
94
|
+
console_inst,
|
|
95
|
+
enable_duplicate_detection,
|
|
96
|
+
title_threshold,
|
|
97
|
+
content_threshold,
|
|
98
|
+
auto_resolve_threshold,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_backend_and_init(core, backend, get_sync_backend_callable):
|
|
103
|
+
"""Resolve backend_type and initialize the sync backend instance.
|
|
104
|
+
|
|
105
|
+
Returns tuple `(backend_type, sync_backend)` where `sync_backend` may
|
|
106
|
+
be None if initialization failed.
|
|
107
|
+
"""
|
|
108
|
+
from roadmap.adapters.cli.sync_context import _resolve_backend_and_init as _ctx
|
|
109
|
+
|
|
110
|
+
return _ctx(core, backend, get_sync_backend_callable)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _clear_baseline_db(core, console_inst):
|
|
114
|
+
"""Clear baseline entries from the SQLite DB if present.
|
|
115
|
+
|
|
116
|
+
Logs warnings but does not raise on failure.
|
|
117
|
+
"""
|
|
118
|
+
from roadmap.adapters.cli.sync_context import _clear_baseline_db as _ctx
|
|
119
|
+
|
|
120
|
+
return _ctx(core, console_inst)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _create_and_save_baseline(
|
|
124
|
+
core, sync_backend, backend_type, console_inst, verbose
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""Create an initial baseline from local issues and save it to DB.
|
|
127
|
+
|
|
128
|
+
Returns True on success, False otherwise.
|
|
129
|
+
"""
|
|
130
|
+
from roadmap.adapters.cli.sync_context import _create_and_save_baseline as _ctx
|
|
131
|
+
|
|
132
|
+
return _ctx(core, sync_backend, backend_type, console_inst, verbose)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _capture_and_save_post_sync_baseline(
|
|
136
|
+
core, console_inst, pre_sync_issue_count, verbose
|
|
137
|
+
):
|
|
138
|
+
"""Capture local issues and save them as the post-sync baseline."""
|
|
139
|
+
from roadmap.adapters.cli.sync_handlers import capture_and_save_post_sync_baseline
|
|
140
|
+
|
|
141
|
+
return capture_and_save_post_sync_baseline(
|
|
142
|
+
core, console_inst, pre_sync_issue_count, verbose
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _perform_apply_phase(
|
|
147
|
+
core,
|
|
148
|
+
orchestrator,
|
|
149
|
+
console_inst,
|
|
150
|
+
analysis_report,
|
|
151
|
+
force_local,
|
|
152
|
+
force_remote,
|
|
153
|
+
push,
|
|
154
|
+
pull,
|
|
155
|
+
verbose,
|
|
156
|
+
):
|
|
157
|
+
"""Run the actual apply phase: perform sync and display summary."""
|
|
158
|
+
from roadmap.adapters.cli.sync_handlers import perform_apply_phase
|
|
159
|
+
|
|
160
|
+
return perform_apply_phase(
|
|
161
|
+
core,
|
|
162
|
+
orchestrator,
|
|
163
|
+
console_inst,
|
|
164
|
+
analysis_report,
|
|
165
|
+
force_local,
|
|
166
|
+
force_remote,
|
|
167
|
+
push,
|
|
168
|
+
pull,
|
|
169
|
+
verbose,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _present_apply_intent(analysis_report, console_inst) -> bool:
|
|
174
|
+
"""Present whether there are changes to apply and return True if apply is needed."""
|
|
175
|
+
from roadmap.adapters.cli.sync_handlers import present_apply_intent
|
|
176
|
+
|
|
177
|
+
return present_apply_intent(analysis_report, console_inst)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _confirm_and_apply(
|
|
181
|
+
core,
|
|
182
|
+
orchestrator,
|
|
183
|
+
console_inst,
|
|
184
|
+
analysis_report,
|
|
185
|
+
force_local,
|
|
186
|
+
force_remote,
|
|
187
|
+
push,
|
|
188
|
+
pull,
|
|
189
|
+
verbose,
|
|
190
|
+
interactive,
|
|
191
|
+
):
|
|
192
|
+
"""Ask for confirmation and run the apply phase if confirmed."""
|
|
193
|
+
from roadmap.adapters.cli.sync_handlers import confirm_and_apply
|
|
194
|
+
|
|
195
|
+
return confirm_and_apply(
|
|
196
|
+
core,
|
|
197
|
+
orchestrator,
|
|
198
|
+
console_inst,
|
|
199
|
+
analysis_report,
|
|
200
|
+
force_local,
|
|
201
|
+
force_remote,
|
|
202
|
+
push,
|
|
203
|
+
pull,
|
|
204
|
+
verbose,
|
|
205
|
+
interactive,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _finalize_sync(
|
|
210
|
+
core,
|
|
211
|
+
console_inst,
|
|
212
|
+
report,
|
|
213
|
+
pre_sync_issue_count,
|
|
214
|
+
verbose,
|
|
215
|
+
backend_type: str | None,
|
|
216
|
+
):
|
|
217
|
+
"""Finalize sync run: capture post-sync baseline and print completion messages."""
|
|
218
|
+
from roadmap.adapters.cli.sync_handlers import finalize_sync
|
|
219
|
+
|
|
220
|
+
finalize_sync(
|
|
221
|
+
core,
|
|
222
|
+
console_inst,
|
|
223
|
+
report,
|
|
224
|
+
pre_sync_issue_count,
|
|
225
|
+
verbose,
|
|
226
|
+
backend_type,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _run_analysis_phase(
|
|
231
|
+
orchestrator,
|
|
232
|
+
push,
|
|
233
|
+
pull,
|
|
234
|
+
dry_run,
|
|
235
|
+
verbose,
|
|
236
|
+
console_inst,
|
|
237
|
+
interactive_duplicates=False,
|
|
238
|
+
):
|
|
239
|
+
"""Run analysis phase using orchestrator and present results.
|
|
240
|
+
|
|
241
|
+
Returns: tuple(plan, analysis_report)
|
|
242
|
+
"""
|
|
243
|
+
from roadmap.adapters.cli.sync_handlers import run_analysis_phase
|
|
244
|
+
|
|
245
|
+
return run_analysis_phase(
|
|
246
|
+
orchestrator, push, pull, dry_run, verbose, console_inst, interactive_duplicates
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_local_status_breakdown(local_only_issues):
|
|
251
|
+
"""Build status breakdown string for local-only issues."""
|
|
252
|
+
from collections import Counter
|
|
253
|
+
|
|
254
|
+
statuses = [c.local_state.status for c in local_only_issues if c.local_state]
|
|
255
|
+
status_counts = Counter(statuses)
|
|
256
|
+
return ", ".join(f"{s}: {count}" for s, count in sorted(status_counts.items()))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _build_remote_status_breakdown(remote_only_issues):
|
|
260
|
+
"""Build status breakdown string for remote-only issues."""
|
|
261
|
+
from collections import Counter
|
|
262
|
+
|
|
263
|
+
statuses = [
|
|
264
|
+
c.remote_state.get("status") for c in remote_only_issues if c.remote_state
|
|
265
|
+
]
|
|
266
|
+
status_counts = Counter(statuses)
|
|
267
|
+
return ", ".join(f"{s}: {count}" for s, count in sorted(status_counts.items()))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _display_local_only_issues(local_only_issues, console_inst):
|
|
271
|
+
"""Display local-only issues in a formatted table."""
|
|
272
|
+
from rich.table import Table
|
|
273
|
+
|
|
274
|
+
console_inst.print(
|
|
275
|
+
"\n[bold cyan]📝 Local-Only Issues[/bold cyan] (exist locally but not remotely)"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
279
|
+
table.add_column("Local ID", style="dim", width=10)
|
|
280
|
+
table.add_column("Title", width=50)
|
|
281
|
+
table.add_column("Status", style="yellow", width=12)
|
|
282
|
+
|
|
283
|
+
for change in sorted(local_only_issues, key=lambda c: c.title):
|
|
284
|
+
status = change.local_state.status if change.local_state else "unknown"
|
|
285
|
+
table.add_row(change.issue_id[:8], change.title, status)
|
|
286
|
+
|
|
287
|
+
console_inst.print(table)
|
|
288
|
+
|
|
289
|
+
# Show status breakdown
|
|
290
|
+
breakdown = _build_local_status_breakdown(local_only_issues)
|
|
291
|
+
console_inst.print(
|
|
292
|
+
f"[dim]Total: {len(local_only_issues)} issues ({breakdown})[/dim]"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _display_remote_only_issues(remote_only_issues, console_inst):
|
|
297
|
+
"""Display remote-only issues in a formatted table with link status."""
|
|
298
|
+
from rich.table import Table
|
|
299
|
+
|
|
300
|
+
console_inst.print(
|
|
301
|
+
"\n[bold cyan]🔄 Remote-Only Issues[/bold cyan] (exist remotely but not locally)"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
305
|
+
table.add_column("Local ID", style="dim", width=10)
|
|
306
|
+
table.add_column("Remote ID", style="cyan", width=12)
|
|
307
|
+
table.add_column("Title", width=40)
|
|
308
|
+
table.add_column("Status", style="yellow", width=10)
|
|
309
|
+
table.add_column("Linked", width=8)
|
|
310
|
+
|
|
311
|
+
# Count linked vs orphaned
|
|
312
|
+
linked_count = 0
|
|
313
|
+
orphaned_count = 0
|
|
314
|
+
|
|
315
|
+
for change in sorted(remote_only_issues, key=lambda c: c.title):
|
|
316
|
+
status = change.remote_state.get("status") if change.remote_state else "unknown"
|
|
317
|
+
# Extract remote ID (backend_id) from remote_state
|
|
318
|
+
remote_id = (
|
|
319
|
+
change.remote_state.get("backend_id") if change.remote_state else "?"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Determine if linked (has a local ID that's not "_remote_")
|
|
323
|
+
is_linked = change.issue_id and not change.issue_id.startswith("_remote_")
|
|
324
|
+
if is_linked:
|
|
325
|
+
linked_icon = "[green]✓[/green]"
|
|
326
|
+
linked_count += 1
|
|
327
|
+
else:
|
|
328
|
+
linked_icon = "[dim red]✗[/dim red]"
|
|
329
|
+
orphaned_count += 1
|
|
330
|
+
|
|
331
|
+
table.add_row(
|
|
332
|
+
change.issue_id[:8] if is_linked else "_remote_",
|
|
333
|
+
str(remote_id),
|
|
334
|
+
change.title,
|
|
335
|
+
status,
|
|
336
|
+
linked_icon,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
console_inst.print(table)
|
|
340
|
+
|
|
341
|
+
# Show status breakdown and link status
|
|
342
|
+
breakdown = _build_remote_status_breakdown(remote_only_issues)
|
|
343
|
+
console_inst.print(
|
|
344
|
+
f"[dim]Total: {len(remote_only_issues)} issues ({breakdown})[/dim]"
|
|
345
|
+
)
|
|
346
|
+
console_inst.print(
|
|
347
|
+
f"[green]Linked to local: {linked_count}[/green] | [dim red]Orphaned (no local match): {orphaned_count}[/dim red]"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _display_issue_lists(core, analysis_report, local_only, remote_only, console_inst):
|
|
352
|
+
"""Display lists of local-only or remote-only issues."""
|
|
353
|
+
if not hasattr(analysis_report, "changes") or not analysis_report.changes:
|
|
354
|
+
console_inst.print("[dim]No issues found[/dim]")
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Get all issues by type
|
|
358
|
+
local_only_issues = [c for c in analysis_report.changes if c.is_local_only_change()]
|
|
359
|
+
remote_only_issues = [
|
|
360
|
+
c for c in analysis_report.changes if c.is_remote_only_change()
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
if local_only and local_only_issues:
|
|
364
|
+
_display_local_only_issues(local_only_issues, console_inst)
|
|
365
|
+
|
|
366
|
+
if remote_only and remote_only_issues:
|
|
367
|
+
_display_remote_only_issues(remote_only_issues, console_inst)
|
|
368
|
+
|
|
369
|
+
if not (local_only or remote_only):
|
|
370
|
+
console_inst.print(
|
|
371
|
+
"[dim]Use --local-only or --remote-only to see issue lists[/dim]"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _clear_baseline(core, backend, console_inst) -> bool:
|
|
376
|
+
"""Handle the `--clear-baseline` flag to clear baseline without syncing."""
|
|
377
|
+
from roadmap.adapters.cli.sync_handlers import clear_baseline
|
|
378
|
+
|
|
379
|
+
return clear_baseline(core, backend, console_inst)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _show_conflicts(core, backend, verbose, console_inst) -> bool:
|
|
383
|
+
"""Handle the `--conflicts` flag to analyze and present conflicts."""
|
|
384
|
+
from roadmap.adapters.cli.sync_handlers import show_conflicts
|
|
385
|
+
|
|
386
|
+
return show_conflicts(core, backend, verbose, console_inst)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _handle_link_unlink(core, backend, link, unlink, issue_id, console_inst) -> bool:
|
|
390
|
+
"""Handle `--link`/`--unlink` operations for manual remote ID management."""
|
|
391
|
+
from roadmap.adapters.cli.sync_handlers import handle_link_unlink
|
|
392
|
+
|
|
393
|
+
return handle_link_unlink(core, backend, link, unlink, issue_id, console_inst)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _handle_pre_sync_actions(
|
|
397
|
+
core,
|
|
398
|
+
backend,
|
|
399
|
+
base: bool,
|
|
400
|
+
reset_baseline: bool,
|
|
401
|
+
clear_baseline: bool,
|
|
402
|
+
conflicts: bool,
|
|
403
|
+
link: str | None,
|
|
404
|
+
unlink: bool,
|
|
405
|
+
issue_id: str | None,
|
|
406
|
+
verbose: bool,
|
|
407
|
+
console_inst,
|
|
408
|
+
) -> bool:
|
|
409
|
+
"""Handle pre-sync CLI actions that may short-circuit the main sync flow."""
|
|
410
|
+
from roadmap.adapters.cli.sync_handlers import handle_pre_sync_actions
|
|
411
|
+
|
|
412
|
+
return handle_pre_sync_actions(
|
|
413
|
+
core,
|
|
414
|
+
backend,
|
|
415
|
+
base,
|
|
416
|
+
reset_baseline,
|
|
417
|
+
clear_baseline,
|
|
418
|
+
conflicts,
|
|
419
|
+
link,
|
|
420
|
+
unlink,
|
|
421
|
+
issue_id,
|
|
422
|
+
verbose,
|
|
423
|
+
console_inst,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@click.group(invoke_without_command=True)
|
|
428
|
+
@click.option(
|
|
429
|
+
"--dry-run",
|
|
430
|
+
is_flag=True,
|
|
431
|
+
help="Preview changes without applying them",
|
|
432
|
+
)
|
|
433
|
+
@click.option(
|
|
434
|
+
"--verbose",
|
|
435
|
+
is_flag=True,
|
|
436
|
+
help="Show detailed sync information",
|
|
437
|
+
)
|
|
438
|
+
@click.option(
|
|
439
|
+
"--force-local",
|
|
440
|
+
is_flag=True,
|
|
441
|
+
help="Resolve all conflicts by keeping local changes",
|
|
442
|
+
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--force-remote",
|
|
445
|
+
is_flag=True,
|
|
446
|
+
help="Resolve all conflicts by keeping remote changes",
|
|
447
|
+
)
|
|
448
|
+
@click.option(
|
|
449
|
+
"--backend",
|
|
450
|
+
type=click.Choice(["github", "git"], case_sensitive=False),
|
|
451
|
+
default=None,
|
|
452
|
+
help="Sync backend to use (auto-detected from config if not specified)",
|
|
453
|
+
)
|
|
454
|
+
@click.option(
|
|
455
|
+
"--base",
|
|
456
|
+
is_flag=True,
|
|
457
|
+
help="Show the baseline state from the database (create if missing)",
|
|
458
|
+
)
|
|
459
|
+
@click.option(
|
|
460
|
+
"--reset-baseline",
|
|
461
|
+
is_flag=True,
|
|
462
|
+
help="Force recalculate baseline from current state (WARNING: resets sync history)",
|
|
463
|
+
)
|
|
464
|
+
@click.option(
|
|
465
|
+
"--clear-baseline",
|
|
466
|
+
is_flag=True,
|
|
467
|
+
help="Clear the baseline without syncing (clears sync_base_state table)",
|
|
468
|
+
)
|
|
469
|
+
@click.option(
|
|
470
|
+
"--conflicts",
|
|
471
|
+
is_flag=True,
|
|
472
|
+
help="Show detailed conflict information between local, remote, and baseline",
|
|
473
|
+
)
|
|
474
|
+
@click.option(
|
|
475
|
+
"--push",
|
|
476
|
+
is_flag=True,
|
|
477
|
+
help="Push local changes to remote only (no pull)",
|
|
478
|
+
)
|
|
479
|
+
@click.option(
|
|
480
|
+
"--pull",
|
|
481
|
+
is_flag=True,
|
|
482
|
+
help="Pull remote changes and merge locally only (no push)",
|
|
483
|
+
)
|
|
484
|
+
@click.option(
|
|
485
|
+
"--link",
|
|
486
|
+
type=str,
|
|
487
|
+
metavar="REMOTE_ID",
|
|
488
|
+
help="Link local issue to remote ID (requires --issue-id)",
|
|
489
|
+
)
|
|
490
|
+
@click.option(
|
|
491
|
+
"--unlink",
|
|
492
|
+
is_flag=True,
|
|
493
|
+
help="Unlink local issue from remote (requires --issue-id)",
|
|
494
|
+
)
|
|
495
|
+
@click.option(
|
|
496
|
+
"--issue-id",
|
|
497
|
+
type=str,
|
|
498
|
+
help="Local issue ID for link/unlink operations",
|
|
499
|
+
)
|
|
500
|
+
@click.option(
|
|
501
|
+
"--baseline",
|
|
502
|
+
type=click.Choice(["local", "remote", "interactive"], case_sensitive=False),
|
|
503
|
+
default=None,
|
|
504
|
+
help="Strategy for first sync baseline (local=local is source, remote=remote is source, interactive=choose per-issue)",
|
|
505
|
+
)
|
|
506
|
+
@click.option(
|
|
507
|
+
"--local-only",
|
|
508
|
+
is_flag=True,
|
|
509
|
+
help="Show issues that exist locally but not remotely",
|
|
510
|
+
)
|
|
511
|
+
@click.option(
|
|
512
|
+
"--remote-only",
|
|
513
|
+
is_flag=True,
|
|
514
|
+
help="Show issues that exist remotely but not locally",
|
|
515
|
+
)
|
|
516
|
+
@click.option(
|
|
517
|
+
"--milestone",
|
|
518
|
+
"-m",
|
|
519
|
+
multiple=True,
|
|
520
|
+
help="Filter sync to specific milestones (can specify multiple)",
|
|
521
|
+
)
|
|
522
|
+
@click.option(
|
|
523
|
+
"--milestone-state",
|
|
524
|
+
type=click.Choice(["open", "closed", "all"], case_sensitive=False),
|
|
525
|
+
default="all",
|
|
526
|
+
help="Filter milestones by state (default: all)",
|
|
527
|
+
)
|
|
528
|
+
@click.option(
|
|
529
|
+
"--since",
|
|
530
|
+
type=str,
|
|
531
|
+
help="Only sync milestones updated since date (ISO format: YYYY-MM-DD)",
|
|
532
|
+
)
|
|
533
|
+
@click.option(
|
|
534
|
+
"--until",
|
|
535
|
+
type=str,
|
|
536
|
+
help="Only sync milestones updated until date (ISO format: YYYY-MM-DD)",
|
|
537
|
+
)
|
|
538
|
+
@click.option(
|
|
539
|
+
"--interactive",
|
|
540
|
+
"-i",
|
|
541
|
+
is_flag=True,
|
|
542
|
+
help="Interactively resolve conflicts with visual diffs and prompts",
|
|
543
|
+
)
|
|
544
|
+
@click.option(
|
|
545
|
+
"--resume",
|
|
546
|
+
is_flag=True,
|
|
547
|
+
help="Resume from last checkpoint after a failed sync",
|
|
548
|
+
)
|
|
549
|
+
@click.option(
|
|
550
|
+
"--detect-duplicates/--no-detect-duplicates",
|
|
551
|
+
default=True,
|
|
552
|
+
help="Enable/disable duplicate detection during sync (default: enabled)",
|
|
553
|
+
)
|
|
554
|
+
@click.option(
|
|
555
|
+
"--interactive-duplicates",
|
|
556
|
+
is_flag=True,
|
|
557
|
+
help="Interactively resolve detected duplicate issues with prompts",
|
|
558
|
+
)
|
|
559
|
+
@click.option(
|
|
560
|
+
"--duplicate-title-threshold",
|
|
561
|
+
type=float,
|
|
562
|
+
default=None,
|
|
563
|
+
help="Minimum similarity for title duplicates (0.0-1.0, default from config)",
|
|
564
|
+
)
|
|
565
|
+
@click.option(
|
|
566
|
+
"--duplicate-content-threshold",
|
|
567
|
+
type=float,
|
|
568
|
+
default=None,
|
|
569
|
+
help="Minimum similarity for content duplicates (0.0-1.0, default from config)",
|
|
570
|
+
)
|
|
571
|
+
@click.option(
|
|
572
|
+
"--duplicate-auto-resolve-threshold",
|
|
573
|
+
type=float,
|
|
574
|
+
default=None,
|
|
575
|
+
help="Minimum confidence for auto-resolving duplicates (0.0-1.0, default from config)",
|
|
576
|
+
)
|
|
577
|
+
@click.option(
|
|
578
|
+
"--show-metrics",
|
|
579
|
+
is_flag=True,
|
|
580
|
+
help="Display sync metrics after completion",
|
|
581
|
+
)
|
|
582
|
+
@click.option(
|
|
583
|
+
"--dedup-only",
|
|
584
|
+
is_flag=True,
|
|
585
|
+
help="Run only the deduplication phase (detect and delete duplicates, skip sync)",
|
|
586
|
+
)
|
|
587
|
+
@click.option(
|
|
588
|
+
"--fuzzy",
|
|
589
|
+
is_flag=True,
|
|
590
|
+
help="Enable fuzzy matching for duplicate detection (slower but more thorough)",
|
|
591
|
+
)
|
|
592
|
+
@click.pass_context
|
|
593
|
+
@require_initialized
|
|
594
|
+
def sync(
|
|
595
|
+
ctx: click.Context,
|
|
596
|
+
dry_run: bool,
|
|
597
|
+
verbose: bool,
|
|
598
|
+
force_local: bool,
|
|
599
|
+
force_remote: bool,
|
|
600
|
+
backend: str | None,
|
|
601
|
+
base: bool,
|
|
602
|
+
reset_baseline: bool,
|
|
603
|
+
clear_baseline: bool,
|
|
604
|
+
conflicts: bool,
|
|
605
|
+
push: bool,
|
|
606
|
+
pull: bool,
|
|
607
|
+
link: str | None,
|
|
608
|
+
unlink: bool,
|
|
609
|
+
issue_id: str | None,
|
|
610
|
+
baseline: str | None,
|
|
611
|
+
local_only: bool,
|
|
612
|
+
remote_only: bool,
|
|
613
|
+
milestone: tuple[str, ...],
|
|
614
|
+
milestone_state: str,
|
|
615
|
+
since: str | None,
|
|
616
|
+
until: str | None,
|
|
617
|
+
interactive: bool,
|
|
618
|
+
resume: bool,
|
|
619
|
+
detect_duplicates: bool,
|
|
620
|
+
interactive_duplicates: bool,
|
|
621
|
+
duplicate_title_threshold: float | None,
|
|
622
|
+
duplicate_content_threshold: float | None,
|
|
623
|
+
duplicate_auto_resolve_threshold: float | None,
|
|
624
|
+
show_metrics: bool,
|
|
625
|
+
dedup_only: bool,
|
|
626
|
+
fuzzy: bool,
|
|
627
|
+
) -> None:
|
|
628
|
+
"""Sync roadmap with remote repository.
|
|
629
|
+
|
|
630
|
+
This command provides intelligent two-way sync with automatic conflict
|
|
631
|
+
resolution using three-way merge. It supports multiple backends:
|
|
632
|
+
|
|
633
|
+
- **github**: Sync with GitHub issues API (requires GitHub configuration)
|
|
634
|
+
- **git**: Sync with Git repository (works with any Git hosting)
|
|
635
|
+
|
|
636
|
+
If no backend is specified, the command auto-detects from your config
|
|
637
|
+
(set during `roadmap init`). GitHub owner/repo can be auto-detected from
|
|
638
|
+
your git remote origin URL.
|
|
639
|
+
|
|
640
|
+
**Sync Process:**
|
|
641
|
+
1. Pull remote changes → merge with local using three-way merge
|
|
642
|
+
2. Resolve conflicts automatically (when possible)
|
|
643
|
+
3. Flag unresolvable conflicts for manual review
|
|
644
|
+
4. Push local changes to remote
|
|
645
|
+
5. User must manually run: git add, git commit, git push
|
|
646
|
+
|
|
647
|
+
**Conflict Resolution:**
|
|
648
|
+
- Critical fields (status, assignee, milestone) → flagged for review
|
|
649
|
+
- Non-critical fields (labels, description) → automatically merged
|
|
650
|
+
- Metadata (timestamps) → remote wins (remote is authoritative)
|
|
651
|
+
|
|
652
|
+
**Milestone Filtering:**
|
|
653
|
+
- Filter sync to specific milestones by name: --milestone "v1.0"
|
|
654
|
+
- Filter by state: --milestone-state open|closed|all
|
|
655
|
+
- Filter by date range: --since 2024-01-01 --until 2024-12-31
|
|
656
|
+
- Combine filters: --milestone "v1.0" --milestone "v2.0" --milestone-state open
|
|
657
|
+
|
|
658
|
+
**Examples:**
|
|
659
|
+
# Sync with GitHub API
|
|
660
|
+
roadmap sync
|
|
661
|
+
|
|
662
|
+
# Preview changes (dry-run, detailed preview)
|
|
663
|
+
roadmap sync --dry-run --verbose
|
|
664
|
+
|
|
665
|
+
# Sync specific milestones only
|
|
666
|
+
roadmap sync --milestone "v1.0" --milestone "v2.0"
|
|
667
|
+
|
|
668
|
+
# Sync only open milestones
|
|
669
|
+
roadmap sync --milestone-state open
|
|
670
|
+
|
|
671
|
+
# Sync milestones updated since a date
|
|
672
|
+
roadmap sync --since 2024-01-01
|
|
673
|
+
|
|
674
|
+
# Sync with verbose output (shows all pulls and pushes)
|
|
675
|
+
roadmap sync --verbose
|
|
676
|
+
|
|
677
|
+
# First sync with local as baseline (local is source of truth)
|
|
678
|
+
roadmap sync --baseline=local
|
|
679
|
+
|
|
680
|
+
# First sync with remote as baseline (remote is source of truth)
|
|
681
|
+
roadmap sync --baseline=remote
|
|
682
|
+
|
|
683
|
+
# First sync with interactive baseline (choose per-issue)
|
|
684
|
+
roadmap sync --baseline=interactive
|
|
685
|
+
|
|
686
|
+
# Resolve all conflicts locally (keep your changes)
|
|
687
|
+
roadmap sync --force-local
|
|
688
|
+
|
|
689
|
+
# Override backend selection
|
|
690
|
+
roadmap sync --backend=github
|
|
691
|
+
"""
|
|
692
|
+
core = ctx.obj["core"]
|
|
693
|
+
console_inst = get_console()
|
|
694
|
+
|
|
695
|
+
# If a subcommand is being invoked, handle it
|
|
696
|
+
if ctx.invoked_subcommand is not None:
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
# Validate mutually exclusive flags
|
|
700
|
+
if push and pull:
|
|
701
|
+
console_inst.print(
|
|
702
|
+
"❌ Cannot use both --push and --pull together. Choose one direction.",
|
|
703
|
+
style="bold red",
|
|
704
|
+
)
|
|
705
|
+
sys.exit(1)
|
|
706
|
+
# Handle early pre-sync actions (baseline display/reset/clear, conflicts, link/unlink)
|
|
707
|
+
if _handle_pre_sync_actions(
|
|
708
|
+
core,
|
|
709
|
+
backend,
|
|
710
|
+
base,
|
|
711
|
+
reset_baseline,
|
|
712
|
+
clear_baseline,
|
|
713
|
+
conflicts,
|
|
714
|
+
link,
|
|
715
|
+
unlink,
|
|
716
|
+
issue_id,
|
|
717
|
+
verbose,
|
|
718
|
+
console_inst,
|
|
719
|
+
):
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
# Handle dedup-only mode
|
|
723
|
+
if dedup_only:
|
|
724
|
+
_execute_dedup_only(
|
|
725
|
+
core,
|
|
726
|
+
console_inst,
|
|
727
|
+
verbose,
|
|
728
|
+
detect_duplicates,
|
|
729
|
+
duplicate_title_threshold,
|
|
730
|
+
duplicate_content_threshold,
|
|
731
|
+
duplicate_auto_resolve_threshold,
|
|
732
|
+
interactive_duplicates,
|
|
733
|
+
fuzzy,
|
|
734
|
+
dry_run,
|
|
735
|
+
)
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
# Handle checkpoint resume if requested
|
|
739
|
+
if resume:
|
|
740
|
+
if not _handle_resume(console_inst, core):
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
try:
|
|
744
|
+
_execute_sync_workflow(
|
|
745
|
+
core,
|
|
746
|
+
console_inst,
|
|
747
|
+
backend,
|
|
748
|
+
baseline,
|
|
749
|
+
dry_run,
|
|
750
|
+
verbose,
|
|
751
|
+
detect_duplicates,
|
|
752
|
+
duplicate_title_threshold,
|
|
753
|
+
duplicate_content_threshold,
|
|
754
|
+
duplicate_auto_resolve_threshold,
|
|
755
|
+
push,
|
|
756
|
+
pull,
|
|
757
|
+
force_local,
|
|
758
|
+
force_remote,
|
|
759
|
+
local_only,
|
|
760
|
+
remote_only,
|
|
761
|
+
milestone,
|
|
762
|
+
milestone_state,
|
|
763
|
+
since,
|
|
764
|
+
until,
|
|
765
|
+
interactive,
|
|
766
|
+
show_metrics,
|
|
767
|
+
)
|
|
768
|
+
except Exception as exc:
|
|
769
|
+
logger.error(
|
|
770
|
+
"sync_command_failed",
|
|
771
|
+
operation="sync",
|
|
772
|
+
error_type=type(exc).__name__,
|
|
773
|
+
error=str(exc),
|
|
774
|
+
severity="operational",
|
|
775
|
+
error_classification="cli_error",
|
|
776
|
+
suggested_action="check_logs_for_details",
|
|
777
|
+
)
|
|
778
|
+
console_inst.print(
|
|
779
|
+
f"❌ Unexpected error during sync: {exc}",
|
|
780
|
+
style="bold red",
|
|
781
|
+
)
|
|
782
|
+
if verbose:
|
|
783
|
+
raise
|
|
784
|
+
sys.exit(1)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _handle_resume(console_inst, core) -> bool:
|
|
788
|
+
"""Handle checkpoint resume logic.
|
|
789
|
+
|
|
790
|
+
Returns True to continue, False to exit.
|
|
791
|
+
"""
|
|
792
|
+
from roadmap.core.services.sync.sync_checkpoint import SyncCheckpointManager
|
|
793
|
+
|
|
794
|
+
checkpoint_manager = SyncCheckpointManager(core)
|
|
795
|
+
can_resume, checkpoint = checkpoint_manager.can_resume()
|
|
796
|
+
|
|
797
|
+
if can_resume and checkpoint:
|
|
798
|
+
console_inst.print("\n[bold cyan]📍 Resuming from checkpoint[/bold cyan]")
|
|
799
|
+
console_inst.print(f"[dim]Checkpoint ID: {checkpoint.checkpoint_id}[/dim]")
|
|
800
|
+
console_inst.print(f"[dim]Phase: {checkpoint.phase}[/dim]")
|
|
801
|
+
console_inst.print(f"[dim]Timestamp: {checkpoint.timestamp}[/dim]")
|
|
802
|
+
|
|
803
|
+
if not click.confirm("\nContinue with resume?", default=True):
|
|
804
|
+
console_inst.print("[yellow]Resume cancelled[/yellow]")
|
|
805
|
+
return False
|
|
806
|
+
return True
|
|
807
|
+
else:
|
|
808
|
+
console_inst.print(
|
|
809
|
+
"[yellow]⚠️ No resumable checkpoint found or checkpoint too old[/yellow]"
|
|
810
|
+
)
|
|
811
|
+
if checkpoint:
|
|
812
|
+
console_inst.print(
|
|
813
|
+
f"[dim]Last checkpoint was in phase '{checkpoint.phase}' at {checkpoint.timestamp}[/dim]"
|
|
814
|
+
)
|
|
815
|
+
return False
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _fetch_remote_for_dedup(core, console_inst, verbose, get_sync_backend):
|
|
819
|
+
"""Initialise sync backend and return (backend, remote_issues_dict).
|
|
820
|
+
|
|
821
|
+
Returns (None, {}) if the backend cannot be authenticated or issues cannot be fetched.
|
|
822
|
+
"""
|
|
823
|
+
from roadmap.adapters.cli.sync_context import _resolve_backend_and_init
|
|
824
|
+
|
|
825
|
+
sync_backend = None
|
|
826
|
+
remote_issues: dict = {}
|
|
827
|
+
try:
|
|
828
|
+
_, sync_backend = _resolve_backend_and_init(core, None, get_sync_backend)
|
|
829
|
+
if sync_backend:
|
|
830
|
+
auth_result = sync_backend.authenticate()
|
|
831
|
+
if not auth_result.is_ok():
|
|
832
|
+
console_inst.print(
|
|
833
|
+
"[yellow]⚠️ Backend authentication failed, skipping remote deduplication[/yellow]"
|
|
834
|
+
)
|
|
835
|
+
return None, {}
|
|
836
|
+
issues_result = sync_backend.get_issues()
|
|
837
|
+
if issues_result.is_ok():
|
|
838
|
+
remote_issues = issues_result.unwrap() or {}
|
|
839
|
+
console_inst.print(f" 📋 Loaded {len(remote_issues)} remote issues")
|
|
840
|
+
if len(remote_issues) > 1000:
|
|
841
|
+
console_inst.print(
|
|
842
|
+
"[dim]ℹ️ Large dataset detected - remote deletion will be slow (respecting GitHub rate limits)[/dim]"
|
|
843
|
+
)
|
|
844
|
+
else:
|
|
845
|
+
console_inst.print(
|
|
846
|
+
"[yellow]⚠️ Failed to fetch remote issues, skipping remote deduplication[/yellow]"
|
|
847
|
+
)
|
|
848
|
+
except Exception as e:
|
|
849
|
+
console_inst.print(f"[yellow]⚠️ Could not initialize backend: {e}[/yellow]")
|
|
850
|
+
if verbose:
|
|
851
|
+
logger.exception("backend_init_failed")
|
|
852
|
+
return sync_backend, remote_issues
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _run_and_display_dedup(
|
|
856
|
+
dedup_service,
|
|
857
|
+
local_issues: list,
|
|
858
|
+
remote_issues: dict,
|
|
859
|
+
dry_run: bool,
|
|
860
|
+
console_inst,
|
|
861
|
+
verbose: bool,
|
|
862
|
+
) -> None:
|
|
863
|
+
"""Run the deduplication service and display results."""
|
|
864
|
+
import time
|
|
865
|
+
|
|
866
|
+
start_time = time.time()
|
|
867
|
+
try:
|
|
868
|
+
response = dedup_service.execute(
|
|
869
|
+
local_issues=local_issues,
|
|
870
|
+
remote_issues=remote_issues,
|
|
871
|
+
dry_run=dry_run,
|
|
872
|
+
)
|
|
873
|
+
elapsed = time.time() - start_time
|
|
874
|
+
console_inst.print()
|
|
875
|
+
console_inst.print("[bold cyan]✨ Deduplication Results[/bold cyan]")
|
|
876
|
+
console_inst.print(f" 🗑️ Deleted: {response.duplicates_removed} duplicates")
|
|
877
|
+
local_remaining = len(response.local_issues)
|
|
878
|
+
remote_remaining = len(response.remote_issues)
|
|
879
|
+
console_inst.print(
|
|
880
|
+
f" 📊 Remaining: {local_remaining} local + {remote_remaining} remote"
|
|
881
|
+
)
|
|
882
|
+
total_before = len(local_issues) + len(remote_issues)
|
|
883
|
+
if total_before > 0:
|
|
884
|
+
total_after = local_remaining + remote_remaining
|
|
885
|
+
reduction_pct = ((total_before - total_after) / total_before) * 100
|
|
886
|
+
console_inst.print(f" 📉 Reduction: {reduction_pct:.1f}%")
|
|
887
|
+
console_inst.print(f" ⏱️ Time: {elapsed:.2f}s")
|
|
888
|
+
console_inst.print()
|
|
889
|
+
console_inst.print("[green]✅ Deduplication complete[/green]")
|
|
890
|
+
except Exception as e:
|
|
891
|
+
console_inst.print(f"❌ Deduplication failed: {e}", style="bold red")
|
|
892
|
+
if verbose:
|
|
893
|
+
raise
|
|
894
|
+
sys.exit(1)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _execute_dedup_only(
|
|
898
|
+
core,
|
|
899
|
+
console_inst,
|
|
900
|
+
verbose,
|
|
901
|
+
detect_duplicates,
|
|
902
|
+
duplicate_title_threshold,
|
|
903
|
+
duplicate_content_threshold,
|
|
904
|
+
duplicate_auto_resolve_threshold,
|
|
905
|
+
interactive_duplicates,
|
|
906
|
+
fuzzy: bool = False,
|
|
907
|
+
dry_run: bool = False,
|
|
908
|
+
) -> None:
|
|
909
|
+
"""Execute deduplication phase only (no sync).
|
|
910
|
+
|
|
911
|
+
Fetches local and remote issues, detects duplicates in both,
|
|
912
|
+
and deletes duplicates (both local and remote via GraphQL).
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
core: RoadmapCore instance
|
|
916
|
+
console_inst: Console instance for output
|
|
917
|
+
verbose: If True, show detailed output
|
|
918
|
+
detect_duplicates: If True, enable duplicate detection
|
|
919
|
+
duplicate_title_threshold: Title similarity threshold override
|
|
920
|
+
duplicate_content_threshold: Content similarity threshold override
|
|
921
|
+
duplicate_auto_resolve_threshold: Auto-resolve confidence threshold
|
|
922
|
+
interactive_duplicates: If True, prompt for duplicate resolution
|
|
923
|
+
fuzzy: If True, enable fuzzy matching (slower but catches more duplicates)
|
|
924
|
+
dry_run: If True, preview duplicates without actually deleting
|
|
925
|
+
"""
|
|
926
|
+
from roadmap.adapters.cli.services.sync_service import get_sync_backend
|
|
927
|
+
from roadmap.application.services.deduplicate_service import DeduplicateService
|
|
928
|
+
from roadmap.core.services.sync.duplicate_detector import DuplicateDetector
|
|
929
|
+
|
|
930
|
+
if not detect_duplicates:
|
|
931
|
+
console_inst.print(
|
|
932
|
+
"[yellow]⚠️ Duplicate detection is disabled (--no-detect-duplicates)[/yellow]"
|
|
933
|
+
)
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
console_inst.print("[bold cyan]🔍 Deduplication Only Mode[/bold cyan]")
|
|
937
|
+
if fuzzy:
|
|
938
|
+
console_inst.print("[yellow]⚠️ Fuzzy matching ENABLED (slower)[/yellow]")
|
|
939
|
+
console_inst.print("[dim]Fetching issues and detecting duplicates...[/dim]\n")
|
|
940
|
+
|
|
941
|
+
try:
|
|
942
|
+
local_issues = core.issue_service.list_all_including_archived() or []
|
|
943
|
+
console_inst.print(f" 📋 Loaded {len(local_issues)} local issues")
|
|
944
|
+
except Exception as e:
|
|
945
|
+
console_inst.print(f"❌ Failed to load local issues: {e}", style="bold red")
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
sync_backend, remote_issues = _fetch_remote_for_dedup(
|
|
949
|
+
core, console_inst, verbose, get_sync_backend
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
detector = DuplicateDetector(
|
|
953
|
+
title_similarity_threshold=duplicate_title_threshold or 0.90,
|
|
954
|
+
content_similarity_threshold=duplicate_content_threshold or 0.85,
|
|
955
|
+
auto_resolve_threshold=duplicate_auto_resolve_threshold or 0.95,
|
|
956
|
+
enable_fuzzy_matching=fuzzy,
|
|
957
|
+
)
|
|
958
|
+
dedup_service = DeduplicateService(
|
|
959
|
+
issue_repo=core.issue_service.repository,
|
|
960
|
+
duplicate_detector=detector,
|
|
961
|
+
backend=sync_backend,
|
|
962
|
+
)
|
|
963
|
+
_run_and_display_dedup(
|
|
964
|
+
dedup_service, local_issues, remote_issues, dry_run, console_inst, verbose
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _execute_sync_workflow(
|
|
969
|
+
core,
|
|
970
|
+
console_inst,
|
|
971
|
+
backend,
|
|
972
|
+
baseline,
|
|
973
|
+
dry_run,
|
|
974
|
+
verbose,
|
|
975
|
+
detect_duplicates,
|
|
976
|
+
duplicate_title_threshold,
|
|
977
|
+
duplicate_content_threshold,
|
|
978
|
+
duplicate_auto_resolve_threshold,
|
|
979
|
+
push,
|
|
980
|
+
pull,
|
|
981
|
+
force_local,
|
|
982
|
+
force_remote,
|
|
983
|
+
local_only,
|
|
984
|
+
remote_only,
|
|
985
|
+
milestone,
|
|
986
|
+
milestone_state,
|
|
987
|
+
since,
|
|
988
|
+
until,
|
|
989
|
+
interactive,
|
|
990
|
+
show_metrics,
|
|
991
|
+
) -> None:
|
|
992
|
+
"""Execute main sync workflow."""
|
|
993
|
+
# Get thresholds from config
|
|
994
|
+
sync_config = core.config_service.get_sync_config()
|
|
995
|
+
title_threshold = (
|
|
996
|
+
duplicate_title_threshold
|
|
997
|
+
if duplicate_title_threshold is not None
|
|
998
|
+
else sync_config["duplicate_title_threshold"]
|
|
999
|
+
)
|
|
1000
|
+
content_threshold = (
|
|
1001
|
+
duplicate_content_threshold
|
|
1002
|
+
if duplicate_content_threshold is not None
|
|
1003
|
+
else sync_config["duplicate_content_threshold"]
|
|
1004
|
+
)
|
|
1005
|
+
auto_resolve_threshold = (
|
|
1006
|
+
duplicate_auto_resolve_threshold
|
|
1007
|
+
if duplicate_auto_resolve_threshold is not None
|
|
1008
|
+
else sync_config["duplicate_auto_resolve_threshold"]
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
# Initialize sync context
|
|
1012
|
+
(
|
|
1013
|
+
backend_type,
|
|
1014
|
+
sync_backend,
|
|
1015
|
+
orchestrator,
|
|
1016
|
+
pre_sync_baseline,
|
|
1017
|
+
pre_sync_issue_count,
|
|
1018
|
+
state_comparator,
|
|
1019
|
+
conflict_resolver,
|
|
1020
|
+
) = _init_sync_context(
|
|
1021
|
+
core,
|
|
1022
|
+
backend,
|
|
1023
|
+
baseline,
|
|
1024
|
+
dry_run,
|
|
1025
|
+
verbose,
|
|
1026
|
+
console_inst,
|
|
1027
|
+
detect_duplicates,
|
|
1028
|
+
title_threshold,
|
|
1029
|
+
content_threshold,
|
|
1030
|
+
auto_resolve_threshold,
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# Run analysis phase
|
|
1034
|
+
plan, analysis_report = _run_analysis_phase(
|
|
1035
|
+
orchestrator,
|
|
1036
|
+
push,
|
|
1037
|
+
pull,
|
|
1038
|
+
dry_run,
|
|
1039
|
+
verbose,
|
|
1040
|
+
console_inst,
|
|
1041
|
+
interactive_duplicates=False,
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
# Display local-only or remote-only lists if requested
|
|
1045
|
+
if local_only or remote_only:
|
|
1046
|
+
_display_issue_lists(
|
|
1047
|
+
core, analysis_report, local_only, remote_only, console_inst
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Handle dry-run
|
|
1051
|
+
if dry_run:
|
|
1052
|
+
from roadmap.adapters.cli.sync_handlers.dry_run_display import (
|
|
1053
|
+
display_detailed_dry_run_preview,
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
display_detailed_dry_run_preview(
|
|
1057
|
+
analysis_report,
|
|
1058
|
+
milestone_filter=milestone,
|
|
1059
|
+
milestone_state=milestone_state,
|
|
1060
|
+
since=since,
|
|
1061
|
+
until=until,
|
|
1062
|
+
verbose=verbose,
|
|
1063
|
+
)
|
|
1064
|
+
return
|
|
1065
|
+
|
|
1066
|
+
# Check if there's anything to apply
|
|
1067
|
+
if not _present_apply_intent(analysis_report, console_inst):
|
|
1068
|
+
return
|
|
1069
|
+
|
|
1070
|
+
# Confirm and apply changes
|
|
1071
|
+
report = _confirm_and_apply(
|
|
1072
|
+
core,
|
|
1073
|
+
orchestrator,
|
|
1074
|
+
console_inst,
|
|
1075
|
+
analysis_report,
|
|
1076
|
+
force_local,
|
|
1077
|
+
force_remote,
|
|
1078
|
+
push,
|
|
1079
|
+
pull,
|
|
1080
|
+
verbose,
|
|
1081
|
+
interactive,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
# User cancelled
|
|
1085
|
+
if report is None:
|
|
1086
|
+
return
|
|
1087
|
+
|
|
1088
|
+
# Finalize real sync
|
|
1089
|
+
_finalize_sync(
|
|
1090
|
+
core,
|
|
1091
|
+
console_inst,
|
|
1092
|
+
report,
|
|
1093
|
+
pre_sync_issue_count,
|
|
1094
|
+
verbose,
|
|
1095
|
+
backend_type,
|
|
1096
|
+
)
|
|
1097
|
+
# Display metrics if requested
|
|
1098
|
+
if show_metrics and hasattr(report, "metrics") and report.metrics:
|
|
1099
|
+
from rich.panel import Panel
|
|
1100
|
+
|
|
1101
|
+
from roadmap.presentation.formatters.sync_metrics_formatter import (
|
|
1102
|
+
create_metrics_summary_table,
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
console_inst.print()
|
|
1106
|
+
console_inst.print(
|
|
1107
|
+
Panel(
|
|
1108
|
+
create_metrics_summary_table(report.metrics, verbose),
|
|
1109
|
+
title="[bold cyan]Sync Metrics[/bold cyan]",
|
|
1110
|
+
border_style="cyan",
|
|
1111
|
+
)
|
|
1112
|
+
)
|
|
1113
|
+
console_inst.print()
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
# Register metrics subcommand
|
|
1117
|
+
sync.add_command(sync_metrics, name="metrics")
|