1use axum::{
2 extract::{Path, Query, State},
3 routing::{get, post},
4 Json, Router,
5};
6use serde::Deserialize;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::Mutex;
10
11use crate::error::ServerError;
12use crate::git;
13use crate::models::worktree::Worktree;
14use crate::state::AppState;
15
16type RepoLocks = Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>;
18
19lazy_static::lazy_static! {
20 static ref REPO_LOCKS: RepoLocks = Arc::new(Mutex::new(HashMap::new()));
21}
22
23pub fn get_repo_locks() -> &'static RepoLocks {
25 &REPO_LOCKS
26}
27
28async fn get_repo_lock(repo_path: &str) -> Arc<Mutex<()>> {
29 let mut locks = REPO_LOCKS.lock().await;
30 locks
31 .entry(repo_path.to_string())
32 .or_insert_with(|| Arc::new(Mutex::new(())))
33 .clone()
34}
35
36pub fn router() -> Router<AppState> {
37 Router::new()
38 .route(
39 "/workspaces/{workspace_id}/codebases/{codebase_id}/worktrees",
40 get(list_worktrees).post(create_worktree),
41 )
42 .route("/worktrees/{id}", get(get_worktree).delete(delete_worktree))
43 .route("/worktrees/{id}/validate", post(validate_worktree))
44}
45
46async fn list_worktrees(
49 State(state): State<AppState>,
50 Path((workspace_id, codebase_id)): Path<(String, String)>,
51) -> Result<Json<serde_json::Value>, ServerError> {
52 let codebase = state
54 .codebase_store
55 .get(&codebase_id)
56 .await?
57 .ok_or_else(|| ServerError::NotFound(format!("Codebase {} not found", codebase_id)))?;
58 if codebase.workspace_id != workspace_id {
59 return Err(ServerError::NotFound(format!(
60 "Codebase {} not found",
61 codebase_id
62 )));
63 }
64
65 let worktrees = state.worktree_store.list_by_codebase(&codebase_id).await?;
66 Ok(Json(serde_json::json!({ "worktrees": worktrees })))
67}
68
69#[derive(Debug, Deserialize)]
72#[serde(rename_all = "camelCase")]
73struct CreateWorktreeRequest {
74 branch: Option<String>,
75 base_branch: Option<String>,
76 label: Option<String>,
77}
78
79async fn create_worktree(
80 State(state): State<AppState>,
81 Path((workspace_id, codebase_id)): Path<(String, String)>,
82 Json(body): Json<CreateWorktreeRequest>,
83) -> Result<Json<serde_json::Value>, ServerError> {
84 let codebase = state
85 .codebase_store
86 .get(&codebase_id)
87 .await?
88 .ok_or_else(|| ServerError::NotFound(format!("Codebase {} not found", codebase_id)))?;
89
90 if codebase.workspace_id != workspace_id {
92 return Err(ServerError::NotFound(format!(
93 "Codebase {} not found",
94 codebase_id
95 )));
96 }
97
98 let repo_path = &codebase.repo_path;
99 let base_branch = body.base_branch.unwrap_or_else(|| {
100 codebase
101 .branch
102 .clone()
103 .unwrap_or_else(|| "main".to_string())
104 });
105
106 let uuid_str = uuid::Uuid::new_v4().to_string();
107 let short_id = &uuid_str[..8];
108 let branch = body.branch.unwrap_or_else(|| {
109 let suffix = body
110 .label
111 .as_ref()
112 .map(|l| git::branch_to_safe_dir_name(l))
113 .unwrap_or_else(|| short_id.to_string());
114 format!("wt/{}", suffix)
115 });
116
117 let lock = get_repo_lock(repo_path).await;
119 let _guard = lock.lock().await;
120
121 if let Some(existing) = state
123 .worktree_store
124 .find_by_branch(&codebase_id, &branch)
125 .await?
126 {
127 return Err(ServerError::Conflict(format!(
128 "Branch '{}' is already in use by worktree {}",
129 branch, existing.id
130 )));
131 }
132
133 let workspace = state.workspace_store.get(&workspace_id).await?;
135 let worktree_root = workspace
136 .as_ref()
137 .and_then(|ws| ws.metadata.get("worktreeRoot"))
138 .filter(|s| !s.trim().is_empty())
139 .map(std::path::PathBuf::from)
140 .unwrap_or_else(|| git::get_default_workspace_worktree_root(&workspace_id));
141
142 let codebase_label = codebase
144 .label
145 .as_ref()
146 .map(|l| git::branch_to_safe_dir_name(l))
147 .unwrap_or_else(|| git::branch_to_safe_dir_name(&codebase_id));
148
149 let worktree_dir = body
151 .label
152 .as_ref()
153 .map(|l| git::branch_to_safe_dir_name(l))
154 .unwrap_or_else(|| git::branch_to_safe_dir_name(&branch));
155 let worktree_path = worktree_root.join(&codebase_label).join(&worktree_dir);
156
157 if let Some(parent) = worktree_path.parent() {
159 std::fs::create_dir_all(parent).map_err(|e| {
160 ServerError::Internal(format!("Failed to create worktree parent dir: {}", e))
161 })?;
162 }
163
164 let worktree_path_str = worktree_path.to_string_lossy().to_string();
165
166 let worktree = Worktree::new(
168 uuid::Uuid::new_v4().to_string(),
169 codebase_id.clone(),
170 codebase.workspace_id.clone(),
171 worktree_path_str.clone(),
172 branch.clone(),
173 base_branch.clone(),
174 body.label,
175 );
176 state.worktree_store.save(&worktree).await?;
177
178 let _ = git::worktree_prune(repo_path);
180
181 let branch_already_exists = git::branch_exists(repo_path, &branch);
183
184 let result = if branch_already_exists {
185 git::worktree_add(repo_path, &worktree_path_str, &branch, &base_branch, false)
186 } else {
187 git::worktree_add(repo_path, &worktree_path_str, &branch, &base_branch, true)
188 };
189
190 match result {
191 Ok(()) => {
192 state
193 .worktree_store
194 .update_status(&worktree.id, "active", None)
195 .await?;
196 let updated = state
197 .worktree_store
198 .get(&worktree.id)
199 .await?
200 .unwrap_or(worktree);
201 Ok(Json(serde_json::json!({ "worktree": updated })))
202 }
203 Err(err) => {
204 state
205 .worktree_store
206 .update_status(&worktree.id, "error", Some(&err))
207 .await?;
208 Err(ServerError::Internal(format!(
209 "Failed to create worktree: {}",
210 err
211 )))
212 }
213 }
214}
215
216async fn get_worktree(
219 State(state): State<AppState>,
220 Path(id): Path<String>,
221) -> Result<Json<serde_json::Value>, ServerError> {
222 let worktree = state
223 .worktree_store
224 .get(&id)
225 .await?
226 .ok_or_else(|| ServerError::NotFound(format!("Worktree {} not found", id)))?;
227 Ok(Json(serde_json::json!({ "worktree": worktree })))
228}
229
230#[derive(Debug, Deserialize, Default)]
233#[serde(rename_all = "camelCase")]
234struct DeleteWorktreeQuery {
235 delete_branch: Option<bool>,
236}
237
238async fn delete_worktree(
239 State(state): State<AppState>,
240 Path(id): Path<String>,
241 Query(query): Query<DeleteWorktreeQuery>,
242) -> Result<Json<serde_json::Value>, ServerError> {
243 let worktree = state
244 .worktree_store
245 .get(&id)
246 .await?
247 .ok_or_else(|| ServerError::NotFound(format!("Worktree {} not found", id)))?;
248
249 let codebase = state.codebase_store.get(&worktree.codebase_id).await?;
250
251 if let Some(codebase) = codebase {
252 let repo_path = &codebase.repo_path;
253 let lock = get_repo_lock(repo_path).await;
254 let _guard = lock.lock().await;
255
256 state
257 .worktree_store
258 .update_status(&id, "removing", None)
259 .await?;
260
261 let _ = git::worktree_remove(repo_path, &worktree.worktree_path, true);
263 let _ = git::worktree_prune(repo_path);
264
265 if query.delete_branch.unwrap_or(false) {
267 let _ = std::process::Command::new("git")
268 .args(["branch", "-D", &worktree.branch])
269 .current_dir(repo_path)
270 .output();
271 }
272 }
273
274 state.worktree_store.delete(&id).await?;
275 Ok(Json(serde_json::json!({ "deleted": true })))
276}
277
278async fn validate_worktree(
281 State(state): State<AppState>,
282 Path(id): Path<String>,
283) -> Result<Json<serde_json::Value>, ServerError> {
284 let worktree = state
285 .worktree_store
286 .get(&id)
287 .await?
288 .ok_or_else(|| ServerError::NotFound(format!("Worktree {} not found", id)))?;
289
290 let path = std::path::Path::new(&worktree.worktree_path);
291 if !path.exists() {
292 state
293 .worktree_store
294 .update_status(&id, "error", Some("Worktree directory missing"))
295 .await?;
296 return Ok(Json(
297 serde_json::json!({ "healthy": false, "error": "Worktree directory missing" }),
298 ));
299 }
300
301 let git_file = path.join(".git");
302 if !git_file.exists() {
303 state
304 .worktree_store
305 .update_status(
306 &id,
307 "error",
308 Some("Not a valid worktree (.git file missing)"),
309 )
310 .await?;
311 return Ok(Json(
312 serde_json::json!({ "healthy": false, "error": "Not a valid worktree (.git file missing)" }),
313 ));
314 }
315
316 if worktree.status == "error" {
318 state
319 .worktree_store
320 .update_status(&id, "active", None)
321 .await?;
322 }
323
324 Ok(Json(serde_json::json!({ "healthy": true })))
325}