summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/mesh_merge.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-06 04:08:11 +0000
committersoryu <soryu@soryu.co>2026-01-11 03:01:13 +0000
commit8b17a175c3e7e27b789812eba4e3cd760beadb10 (patch)
tree7864dcaa2fa9db47fdfd4e8bfdb0b1dde832aa33 /makima/src/server/handlers/mesh_merge.rs
parentf79c416c58557d2f946aa5332989afdfa8c021cd (diff)
downloadsoryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.tar.gz
soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.zip
Initial Control system
Diffstat (limited to 'makima/src/server/handlers/mesh_merge.rs')
-rw-r--r--makima/src/server/handlers/mesh_merge.rs441
1 files changed, 441 insertions, 0 deletions
diff --git a/makima/src/server/handlers/mesh_merge.rs b/makima/src/server/handlers/mesh_merge.rs
new file mode 100644
index 0000000..2d7c742
--- /dev/null
+++ b/makima/src/server/handlers/mesh_merge.rs
@@ -0,0 +1,441 @@
+//! Merge operation handlers for orchestrator tasks.
+//!
+//! These endpoints allow orchestrators to merge subtask branches.
+//! Commands are forwarded to the daemon via WebSocket; the daemon
+//! responds asynchronously through the WebSocket channel.
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use uuid::Uuid;
+
+use crate::db::models::{
+ BranchListResponse, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest,
+ MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse,
+};
+use crate::db::repository;
+use crate::server::messages::ApiError;
+use crate::server::state::{DaemonCommand, SharedState};
+
+/// Get the daemon ID for a task, returning error if not found.
+async fn get_task_daemon_id(
+ state: &SharedState,
+ task_id: Uuid,
+) -> Result<Uuid, (StatusCode, Json<ApiError>)> {
+ let pool = state.db_pool.as_ref().ok_or_else(|| {
+ (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("service_unavailable", "Database not configured")),
+ )
+ })?;
+
+ // Get task and its daemon_id
+ let task = repository::get_task(pool, task_id)
+ .await
+ .map_err(|e| {
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("database_error", format!("Database error: {}", e))),
+ )
+ })?
+ .ok_or_else(|| {
+ (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("not_found", format!("Task {} not found", task_id))),
+ )
+ })?;
+
+ task.daemon_id.ok_or_else(|| {
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("bad_request", "Task has no assigned daemon")),
+ )
+ })
+}
+
+/// List all subtask branches for a task.
+///
+/// GET /api/v1/mesh/tasks/{id}/branches
+#[utoipa::path(
+ get,
+ path = "/api/v1/mesh/tasks/{id}/branches",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ responses(
+ (status = 202, description = "Command sent to daemon"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured")
+ ),
+ tag = "Mesh"
+)]
+pub async fn list_branches(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::ListBranches { task_id };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(BranchListResponse { branches: vec![] }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Start merging a subtask branch.
+///
+/// POST /api/v1/mesh/tasks/{id}/merge/start
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/tasks/{id}/merge/start",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ request_body = MergeStartRequest,
+ responses(
+ (status = 202, description = "Merge command sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_start(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+ Json(req): Json<MergeStartRequest>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::MergeStart {
+ task_id,
+ source_branch: req.source_branch,
+ };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeResultResponse {
+ success: true,
+ message: "Merge command sent".to_string(),
+ commit_sha: None,
+ conflicts: None,
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Get current merge status.
+///
+/// GET /api/v1/mesh/tasks/{id}/merge/status
+#[utoipa::path(
+ get,
+ path = "/api/v1/mesh/tasks/{id}/merge/status",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ responses(
+ (status = 202, description = "Status request sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_status(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::MergeStatus { task_id };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeStatusResponse {
+ in_progress: false,
+ source_branch: None,
+ conflicted_files: vec![],
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Resolve a merge conflict.
+///
+/// POST /api/v1/mesh/tasks/{id}/merge/resolve
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/tasks/{id}/merge/resolve",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ request_body = MergeResolveRequest,
+ responses(
+ (status = 202, description = "Resolve command sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_resolve(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+ Json(req): Json<MergeResolveRequest>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::MergeResolve {
+ task_id,
+ file: req.file,
+ strategy: req.strategy,
+ };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeResultResponse {
+ success: true,
+ message: "Resolve command sent".to_string(),
+ commit_sha: None,
+ conflicts: None,
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Commit the current merge.
+///
+/// POST /api/v1/mesh/tasks/{id}/merge/commit
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/tasks/{id}/merge/commit",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ request_body = MergeCommitRequest,
+ responses(
+ (status = 202, description = "Commit command sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_commit(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+ Json(req): Json<MergeCommitRequest>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::MergeCommit {
+ task_id,
+ message: req.message,
+ };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeResultResponse {
+ success: true,
+ message: "Commit command sent".to_string(),
+ commit_sha: None,
+ conflicts: None,
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Abort the current merge.
+///
+/// POST /api/v1/mesh/tasks/{id}/merge/abort
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/tasks/{id}/merge/abort",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ responses(
+ (status = 202, description = "Abort command sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_abort(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::MergeAbort { task_id };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeResultResponse {
+ success: true,
+ message: "Abort command sent".to_string(),
+ commit_sha: None,
+ conflicts: None,
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Skip merging a subtask branch.
+///
+/// POST /api/v1/mesh/tasks/{id}/merge/skip
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/tasks/{id}/merge/skip",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ request_body = MergeSkipRequest,
+ responses(
+ (status = 202, description = "Skip command sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_skip(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+ Json(req): Json<MergeSkipRequest>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::MergeSkip {
+ task_id,
+ subtask_id: req.subtask_id,
+ reason: req.reason,
+ };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeResultResponse {
+ success: true,
+ message: "Skip command sent".to_string(),
+ commit_sha: None,
+ conflicts: None,
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}
+
+/// Check if all branches are merged or skipped.
+///
+/// GET /api/v1/mesh/tasks/{id}/merge/check
+#[utoipa::path(
+ get,
+ path = "/api/v1/mesh/tasks/{id}/merge/check",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ responses(
+ (status = 202, description = "Check command sent"),
+ (status = 404, description = "Task not found"),
+ (status = 503, description = "Database not configured or daemon not connected")
+ ),
+ tag = "Mesh"
+)]
+pub async fn merge_check(
+ State(state): State<SharedState>,
+ Path(task_id): Path<Uuid>,
+) -> impl IntoResponse {
+ let daemon_id = match get_task_daemon_id(&state, task_id).await {
+ Ok(id) => id,
+ Err(e) => return e.into_response(),
+ };
+
+ let command = DaemonCommand::CheckMergeComplete { task_id };
+
+ match state.send_daemon_command(daemon_id, command).await {
+ Ok(()) => (
+ StatusCode::ACCEPTED,
+ Json(MergeCompleteCheckResponse {
+ can_complete: true,
+ unmerged_branches: vec![],
+ merged_count: 0,
+ skipped_count: 0,
+ }),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("daemon_error", e)),
+ )
+ .into_response(),
+ }
+}