Skip to main content

routa_server/api/
codebases.rs

1use axum::{
2    extract::State,
3    routing::{get, patch, post},
4    Json, Router,
5};
6use serde::Deserialize;
7
8use crate::error::ServerError;
9use crate::models::codebase::Codebase;
10use crate::state::AppState;
11
12pub fn router() -> Router<AppState> {
13    Router::new()
14        .route(
15            "/workspaces/{workspace_id}/codebases",
16            get(list_codebases).post(add_codebase),
17        )
18        .route(
19            "/codebases/{id}",
20            patch(update_codebase).delete(delete_codebase),
21        )
22        .route("/codebases/{id}/default", post(set_default_codebase))
23}
24
25async fn list_codebases(
26    State(state): State<AppState>,
27    axum::extract::Path(workspace_id): axum::extract::Path<String>,
28) -> Result<Json<serde_json::Value>, ServerError> {
29    let codebases = state
30        .codebase_store
31        .list_by_workspace(&workspace_id)
32        .await?;
33    Ok(Json(serde_json::json!({ "codebases": codebases })))
34}
35
36#[derive(Debug, Deserialize)]
37#[serde(rename_all = "camelCase")]
38struct AddCodebaseRequest {
39    repo_path: String,
40    branch: Option<String>,
41    label: Option<String>,
42    #[serde(default)]
43    is_default: bool,
44}
45
46async fn add_codebase(
47    State(state): State<AppState>,
48    axum::extract::Path(workspace_id): axum::extract::Path<String>,
49    Json(body): Json<AddCodebaseRequest>,
50) -> Result<Json<serde_json::Value>, ServerError> {
51    // Check for duplicate repo_path within the workspace
52    if let Some(_existing) = state
53        .codebase_store
54        .find_by_repo_path(&workspace_id, &body.repo_path)
55        .await?
56    {
57        return Err(ServerError::Conflict(format!(
58            "Codebase with repo_path '{}' already exists in workspace {}",
59            body.repo_path, workspace_id
60        )));
61    }
62
63    let codebase = Codebase::new(
64        uuid::Uuid::new_v4().to_string(),
65        workspace_id,
66        body.repo_path,
67        body.branch,
68        body.label,
69        body.is_default,
70    );
71
72    state.codebase_store.save(&codebase).await?;
73    Ok(Json(serde_json::json!({ "codebase": codebase })))
74}
75
76#[derive(Debug, Deserialize)]
77#[serde(rename_all = "camelCase")]
78struct UpdateCodebaseRequest {
79    branch: Option<String>,
80    label: Option<String>,
81    repo_path: Option<String>,
82}
83
84async fn update_codebase(
85    State(state): State<AppState>,
86    axum::extract::Path(id): axum::extract::Path<String>,
87    Json(body): Json<UpdateCodebaseRequest>,
88) -> Result<Json<serde_json::Value>, ServerError> {
89    // Verify codebase exists
90    state
91        .codebase_store
92        .get(&id)
93        .await?
94        .ok_or_else(|| ServerError::NotFound(format!("Codebase {} not found", id)))?;
95
96    state
97        .codebase_store
98        .update(
99            &id,
100            body.branch.as_deref(),
101            body.label.as_deref(),
102            body.repo_path.as_deref(),
103        )
104        .await?;
105
106    let codebase = state
107        .codebase_store
108        .get(&id)
109        .await?
110        .ok_or_else(|| ServerError::NotFound(format!("Codebase {} not found", id)))?;
111
112    Ok(Json(serde_json::json!({ "codebase": codebase })))
113}
114
115async fn delete_codebase(
116    State(state): State<AppState>,
117    axum::extract::Path(id): axum::extract::Path<String>,
118) -> Result<Json<serde_json::Value>, ServerError> {
119    // Clean up worktrees on disk before deleting the codebase
120    if let Ok(Some(codebase)) = state.codebase_store.get(&id).await {
121        let repo_path = &codebase.repo_path;
122
123        // Acquire repo lock to prevent races with concurrent worktree operations
124        let lock = {
125            let mut locks = crate::api::worktrees::get_repo_locks().lock().await;
126            locks
127                .entry(repo_path.to_string())
128                .or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(())))
129                .clone()
130        };
131        let _guard = lock.lock().await;
132
133        let worktrees = state
134            .worktree_store
135            .list_by_codebase(&id)
136            .await
137            .map_err(|e| ServerError::Internal(format!("Failed to list worktrees: {}", e)))?;
138        for wt in &worktrees {
139            if let Err(e) = crate::git::worktree_remove(repo_path, &wt.worktree_path, true) {
140                tracing::warn!(
141                    "[Codebase DELETE] Failed to remove worktree {}: {}",
142                    wt.id,
143                    e
144                );
145            }
146        }
147        if !worktrees.is_empty() {
148            let _ = crate::git::worktree_prune(repo_path);
149        }
150    }
151
152    state.codebase_store.delete(&id).await?;
153    Ok(Json(serde_json::json!({ "deleted": true })))
154}
155
156async fn set_default_codebase(
157    State(state): State<AppState>,
158    axum::extract::Path(id): axum::extract::Path<String>,
159) -> Result<Json<serde_json::Value>, ServerError> {
160    let codebase = state
161        .codebase_store
162        .get(&id)
163        .await?
164        .ok_or_else(|| ServerError::NotFound(format!("Codebase {} not found", id)))?;
165
166    state
167        .codebase_store
168        .set_default(&codebase.workspace_id, &id)
169        .await?;
170
171    let updated = state
172        .codebase_store
173        .get(&id)
174        .await?
175        .ok_or_else(|| ServerError::NotFound(format!("Codebase {} not found", id)))?;
176
177    Ok(Json(serde_json::json!({ "codebase": updated })))
178}