routa_server/api/
codebases.rs1use 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 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 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 if let Ok(Some(codebase)) = state.codebase_store.get(&id).await {
121 let repo_path = &codebase.repo_path;
122
123 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}