draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""API router for merge and conflict resolution operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
7
|
+
from sqlalchemy import select
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
9
|
+
from sqlalchemy.orm import selectinload
|
|
10
|
+
|
|
11
|
+
from app.database import get_db
|
|
12
|
+
from app.exceptions import ConflictError, ResourceNotFoundError, ValidationError
|
|
13
|
+
from app.models.board import Board
|
|
14
|
+
from app.models.ticket import Ticket
|
|
15
|
+
from app.schemas.merge import (
|
|
16
|
+
AbortResponse,
|
|
17
|
+
ConflictStatusResponse,
|
|
18
|
+
MergeRequest,
|
|
19
|
+
MergeResponse,
|
|
20
|
+
MergeStatusResponse,
|
|
21
|
+
PushResponse,
|
|
22
|
+
PushStatusResponse,
|
|
23
|
+
RebaseResponse,
|
|
24
|
+
)
|
|
25
|
+
from app.services.merge_service import MergeService, MergeStrategy
|
|
26
|
+
|
|
27
|
+
router = APIRouter(prefix="/tickets", tags=["merge"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _get_ticket_worktree(ticket_id: str, db: AsyncSession) -> tuple[Ticket, Path]:
|
|
31
|
+
"""Get ticket and its active worktree path. Raises HTTP errors."""
|
|
32
|
+
result = await db.execute(
|
|
33
|
+
select(Ticket)
|
|
34
|
+
.where(Ticket.id == ticket_id)
|
|
35
|
+
.options(selectinload(Ticket.workspace))
|
|
36
|
+
)
|
|
37
|
+
ticket = result.scalar_one_or_none()
|
|
38
|
+
if not ticket:
|
|
39
|
+
raise HTTPException(status_code=404, detail=f"Ticket {ticket_id} not found")
|
|
40
|
+
if not ticket.workspace or not ticket.workspace.is_active:
|
|
41
|
+
raise HTTPException(status_code=404, detail="Ticket has no active worktree")
|
|
42
|
+
|
|
43
|
+
worktree_path = Path(ticket.workspace.worktree_path)
|
|
44
|
+
if not worktree_path.exists():
|
|
45
|
+
raise HTTPException(status_code=404, detail="Worktree directory not found")
|
|
46
|
+
|
|
47
|
+
return ticket, worktree_path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.post(
|
|
51
|
+
"/{ticket_id}/merge",
|
|
52
|
+
response_model=MergeResponse,
|
|
53
|
+
summary="Merge a ticket's changes into the default branch",
|
|
54
|
+
responses={
|
|
55
|
+
200: {"description": "Merge completed (check success field)"},
|
|
56
|
+
404: {"description": "Ticket not found"},
|
|
57
|
+
409: {"description": "Merge conflict or validation error"},
|
|
58
|
+
422: {"description": "Invalid request"},
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
async def merge_ticket(
|
|
62
|
+
ticket_id: str,
|
|
63
|
+
data: MergeRequest,
|
|
64
|
+
db: AsyncSession = Depends(get_db),
|
|
65
|
+
) -> MergeResponse:
|
|
66
|
+
"""
|
|
67
|
+
Merge a ticket's worktree branch into the default branch.
|
|
68
|
+
|
|
69
|
+
Prerequisites:
|
|
70
|
+
- Ticket must be in 'done' state
|
|
71
|
+
- Ticket must have an approved revision
|
|
72
|
+
- Ticket must have an active worktree
|
|
73
|
+
|
|
74
|
+
The merge will:
|
|
75
|
+
1. Verify the worktree has no uncommitted changes
|
|
76
|
+
2. Checkout the default branch in the main repo
|
|
77
|
+
3. Pull latest changes (optional, based on config)
|
|
78
|
+
4. Merge or rebase the worktree branch
|
|
79
|
+
5. Optionally delete the worktree and cleanup artifacts
|
|
80
|
+
|
|
81
|
+
If the merge fails due to conflicts, the merge is aborted and the
|
|
82
|
+
worktree is left intact for manual resolution.
|
|
83
|
+
"""
|
|
84
|
+
# Load board config for merge settings
|
|
85
|
+
board_config = None
|
|
86
|
+
ticket_result = await db.execute(select(Ticket).where(Ticket.id == ticket_id))
|
|
87
|
+
ticket_obj = ticket_result.scalar_one_or_none()
|
|
88
|
+
if ticket_obj and ticket_obj.board_id:
|
|
89
|
+
board_result = await db.execute(
|
|
90
|
+
select(Board).where(Board.id == ticket_obj.board_id)
|
|
91
|
+
)
|
|
92
|
+
board_obj = board_result.scalar_one_or_none()
|
|
93
|
+
if board_obj:
|
|
94
|
+
board_config = board_obj.config
|
|
95
|
+
|
|
96
|
+
service = MergeService(db, board_config=board_config)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Convert schema enum to service enum
|
|
100
|
+
strategy = (
|
|
101
|
+
MergeStrategy.MERGE
|
|
102
|
+
if data.strategy.value == "merge"
|
|
103
|
+
else MergeStrategy.REBASE
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
result = await service.merge_ticket(
|
|
107
|
+
ticket_id=ticket_id,
|
|
108
|
+
strategy=strategy,
|
|
109
|
+
delete_worktree=data.delete_worktree,
|
|
110
|
+
cleanup_artifacts=data.cleanup_artifacts,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return MergeResponse(
|
|
114
|
+
success=result.success,
|
|
115
|
+
message=result.message,
|
|
116
|
+
exit_code=result.exit_code,
|
|
117
|
+
evidence_id=result.evidence_ids.get("meta_id")
|
|
118
|
+
if result.evidence_ids
|
|
119
|
+
else None,
|
|
120
|
+
pull_warning=result.pull_warning,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
except ResourceNotFoundError as e:
|
|
124
|
+
raise HTTPException(
|
|
125
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
126
|
+
detail=str(e),
|
|
127
|
+
) from e
|
|
128
|
+
except ValidationError as e:
|
|
129
|
+
raise HTTPException(
|
|
130
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
131
|
+
detail=str(e),
|
|
132
|
+
) from e
|
|
133
|
+
except ConflictError as e:
|
|
134
|
+
raise HTTPException(
|
|
135
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
136
|
+
detail=str(e),
|
|
137
|
+
) from e
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.get(
|
|
141
|
+
"/{ticket_id}/merge-status",
|
|
142
|
+
response_model=MergeStatusResponse,
|
|
143
|
+
summary="Get merge status for a ticket",
|
|
144
|
+
)
|
|
145
|
+
async def get_merge_status(
|
|
146
|
+
ticket_id: str,
|
|
147
|
+
db: AsyncSession = Depends(get_db),
|
|
148
|
+
) -> MergeStatusResponse:
|
|
149
|
+
"""
|
|
150
|
+
Get the merge status for a ticket.
|
|
151
|
+
|
|
152
|
+
Returns whether the ticket can be merged, whether it has already been
|
|
153
|
+
merged, and information about the workspace and last merge attempt.
|
|
154
|
+
"""
|
|
155
|
+
service = MergeService(db)
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
status_info = await service.get_merge_status(ticket_id)
|
|
159
|
+
return MergeStatusResponse(**status_info)
|
|
160
|
+
except ResourceNotFoundError as e:
|
|
161
|
+
raise HTTPException(
|
|
162
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
163
|
+
detail=str(e),
|
|
164
|
+
) from e
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ===================== Conflict Resolution Endpoints =====================
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@router.get(
|
|
171
|
+
"/{ticket_id}/conflict-status",
|
|
172
|
+
response_model=ConflictStatusResponse,
|
|
173
|
+
summary="Check if a ticket's worktree has conflicts",
|
|
174
|
+
)
|
|
175
|
+
async def get_conflict_status(
|
|
176
|
+
ticket_id: str,
|
|
177
|
+
db: AsyncSession = Depends(get_db),
|
|
178
|
+
) -> ConflictStatusResponse:
|
|
179
|
+
"""Check for conflicts in the ticket's worktree.
|
|
180
|
+
|
|
181
|
+
Returns conflict state, affected files, and whether continue/abort is possible.
|
|
182
|
+
Also returns branch divergence info for merge planning.
|
|
183
|
+
"""
|
|
184
|
+
from app.services.git_ops import detect_conflict_state, get_divergence_info
|
|
185
|
+
|
|
186
|
+
ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
187
|
+
|
|
188
|
+
state = await asyncio.to_thread(detect_conflict_state, worktree_path)
|
|
189
|
+
|
|
190
|
+
# Get divergence info
|
|
191
|
+
from app.services.workspace_service import WorkspaceService
|
|
192
|
+
|
|
193
|
+
repo_path = WorkspaceService.get_repo_path()
|
|
194
|
+
branch_name = ticket.workspace.branch_name
|
|
195
|
+
divergence = await asyncio.to_thread(get_divergence_info, repo_path, branch_name)
|
|
196
|
+
|
|
197
|
+
if state is None:
|
|
198
|
+
return ConflictStatusResponse(
|
|
199
|
+
has_conflict=False,
|
|
200
|
+
operation=None,
|
|
201
|
+
conflicted_files=[],
|
|
202
|
+
can_continue=False,
|
|
203
|
+
can_abort=False,
|
|
204
|
+
divergence=divergence,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return ConflictStatusResponse(
|
|
208
|
+
has_conflict=True,
|
|
209
|
+
operation=state.operation,
|
|
210
|
+
conflicted_files=state.conflicted_files,
|
|
211
|
+
can_continue=state.can_continue,
|
|
212
|
+
can_abort=state.can_abort,
|
|
213
|
+
divergence=divergence,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@router.post(
|
|
218
|
+
"/{ticket_id}/rebase",
|
|
219
|
+
response_model=RebaseResponse,
|
|
220
|
+
summary="Rebase a ticket's branch onto the target branch",
|
|
221
|
+
)
|
|
222
|
+
async def rebase_ticket(
|
|
223
|
+
ticket_id: str,
|
|
224
|
+
onto_branch: str = "main",
|
|
225
|
+
db: AsyncSession = Depends(get_db),
|
|
226
|
+
) -> RebaseResponse:
|
|
227
|
+
"""Rebase the ticket's worktree branch onto the target branch.
|
|
228
|
+
|
|
229
|
+
Use this when the base branch has moved forward (divergence detected).
|
|
230
|
+
If conflicts arise, use continue-rebase or abort-conflict.
|
|
231
|
+
"""
|
|
232
|
+
from app.services.git_ops import rebase_branch
|
|
233
|
+
|
|
234
|
+
_, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
235
|
+
|
|
236
|
+
result = await asyncio.to_thread(rebase_branch, worktree_path, onto_branch)
|
|
237
|
+
|
|
238
|
+
return RebaseResponse(
|
|
239
|
+
success=result.success,
|
|
240
|
+
message=result.message,
|
|
241
|
+
has_conflicts=result.has_conflicts,
|
|
242
|
+
conflicted_files=result.conflicted_files,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@router.post(
|
|
247
|
+
"/{ticket_id}/continue-rebase",
|
|
248
|
+
response_model=RebaseResponse,
|
|
249
|
+
summary="Continue a paused rebase after resolving conflicts",
|
|
250
|
+
)
|
|
251
|
+
async def continue_rebase_endpoint(
|
|
252
|
+
ticket_id: str,
|
|
253
|
+
db: AsyncSession = Depends(get_db),
|
|
254
|
+
) -> RebaseResponse:
|
|
255
|
+
"""Continue a rebase that paused due to conflicts.
|
|
256
|
+
|
|
257
|
+
Call this after the AI agent (or user) has resolved conflicts in the worktree.
|
|
258
|
+
"""
|
|
259
|
+
from app.services.git_ops import continue_rebase
|
|
260
|
+
|
|
261
|
+
_, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
262
|
+
|
|
263
|
+
result = await asyncio.to_thread(continue_rebase, worktree_path)
|
|
264
|
+
|
|
265
|
+
return RebaseResponse(
|
|
266
|
+
success=result.success,
|
|
267
|
+
message=result.message,
|
|
268
|
+
has_conflicts=result.has_conflicts,
|
|
269
|
+
conflicted_files=result.conflicted_files,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@router.post(
|
|
274
|
+
"/{ticket_id}/abort-conflict",
|
|
275
|
+
response_model=AbortResponse,
|
|
276
|
+
summary="Abort the current conflict operation",
|
|
277
|
+
)
|
|
278
|
+
async def abort_conflict_endpoint(
|
|
279
|
+
ticket_id: str,
|
|
280
|
+
db: AsyncSession = Depends(get_db),
|
|
281
|
+
) -> AbortResponse:
|
|
282
|
+
"""Abort the current conflict operation (rebase, merge, cherry-pick, etc.).
|
|
283
|
+
|
|
284
|
+
Returns the worktree to its pre-operation state.
|
|
285
|
+
"""
|
|
286
|
+
from app.services.git_ops import abort_operation
|
|
287
|
+
|
|
288
|
+
_, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
289
|
+
|
|
290
|
+
success = await asyncio.to_thread(abort_operation, worktree_path)
|
|
291
|
+
|
|
292
|
+
return AbortResponse(
|
|
293
|
+
success=success,
|
|
294
|
+
message="Operation aborted successfully"
|
|
295
|
+
if success
|
|
296
|
+
else "Failed to abort operation",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ===================== Push Endpoints =====================
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@router.get(
|
|
304
|
+
"/{ticket_id}/push-status",
|
|
305
|
+
response_model=PushStatusResponse,
|
|
306
|
+
summary="Check if a ticket's branch needs to be pushed",
|
|
307
|
+
)
|
|
308
|
+
async def get_push_status_endpoint(
|
|
309
|
+
ticket_id: str,
|
|
310
|
+
db: AsyncSession = Depends(get_db),
|
|
311
|
+
) -> PushStatusResponse:
|
|
312
|
+
"""Check if the ticket's branch is ahead/behind the remote tracking branch."""
|
|
313
|
+
from app.services.git_ops import get_push_status
|
|
314
|
+
|
|
315
|
+
ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
316
|
+
branch_name = ticket.workspace.branch_name
|
|
317
|
+
|
|
318
|
+
result = await asyncio.to_thread(get_push_status, worktree_path, branch_name)
|
|
319
|
+
return PushStatusResponse(**result)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@router.post(
|
|
323
|
+
"/{ticket_id}/push",
|
|
324
|
+
response_model=PushResponse,
|
|
325
|
+
summary="Push a ticket's branch to remote",
|
|
326
|
+
)
|
|
327
|
+
async def push_ticket_branch(
|
|
328
|
+
ticket_id: str,
|
|
329
|
+
db: AsyncSession = Depends(get_db),
|
|
330
|
+
) -> PushResponse:
|
|
331
|
+
"""Push the ticket's worktree branch to the remote origin."""
|
|
332
|
+
from app.services.git_ops import push_branch
|
|
333
|
+
|
|
334
|
+
ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
335
|
+
branch_name = ticket.workspace.branch_name
|
|
336
|
+
|
|
337
|
+
result = await asyncio.to_thread(push_branch, worktree_path, branch_name)
|
|
338
|
+
return PushResponse(success=result.success, message=result.message)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@router.post(
|
|
342
|
+
"/{ticket_id}/force-push",
|
|
343
|
+
response_model=PushResponse,
|
|
344
|
+
summary="Force-push a ticket's branch to remote (with lease)",
|
|
345
|
+
)
|
|
346
|
+
async def force_push_ticket_branch(
|
|
347
|
+
ticket_id: str,
|
|
348
|
+
db: AsyncSession = Depends(get_db),
|
|
349
|
+
) -> PushResponse:
|
|
350
|
+
"""Force-push the ticket's branch using --force-with-lease for safety.
|
|
351
|
+
|
|
352
|
+
Use after rebase when the remote branch already exists.
|
|
353
|
+
"""
|
|
354
|
+
from app.services.git_ops import force_push_branch
|
|
355
|
+
|
|
356
|
+
ticket, worktree_path = await _get_ticket_worktree(ticket_id, db)
|
|
357
|
+
branch_name = ticket.workspace.branch_name
|
|
358
|
+
|
|
359
|
+
result = await asyncio.to_thread(force_push_branch, worktree_path, branch_name)
|
|
360
|
+
return PushResponse(success=result.success, message=result.message)
|