Skip to main content

routa_server/api/
clone_branches.rs

1//! Branch Management API - /api/clone/branches
2//!
3//! GET   /api/clone/branches?repoPath=... - Get branch info
4//! POST  /api/clone/branches - Fetch remote branches then return all
5//! PATCH /api/clone/branches - Checkout a branch
6//! DELETE /api/clone/branches - Delete a local branch
7
8use axum::{extract::Query, routing::get, Json, Router};
9use serde::Deserialize;
10
11use crate::error::ServerError;
12use crate::git;
13use crate::state::AppState;
14
15pub fn router() -> Router<AppState> {
16    Router::new().route(
17        "/",
18        get(get_branches)
19            .post(fetch_branches)
20            .patch(checkout)
21            .delete(delete_branch),
22    )
23}
24
25#[derive(Debug, Deserialize)]
26#[serde(rename_all = "camelCase")]
27struct BranchQuery {
28    repo_path: Option<String>,
29}
30
31async fn get_branches(
32    Query(query): Query<BranchQuery>,
33) -> Result<Json<serde_json::Value>, ServerError> {
34    let repo_path = query
35        .repo_path
36        .ok_or_else(|| ServerError::BadRequest("Missing repoPath".into()))?;
37
38    if !std::path::Path::new(&repo_path).exists() {
39        return Err(ServerError::BadRequest(
40            "Missing or invalid repoPath".into(),
41        ));
42    }
43
44    let (current, local, remote, status) = tokio::task::spawn_blocking({
45        let rp = repo_path.clone();
46        move || {
47            let current = git::get_current_branch(&rp).unwrap_or_else(|| "unknown".into());
48            let local = git::list_local_branches(&rp);
49            let remote = git::list_remote_branches(&rp);
50            let status = git::get_branch_status(&rp, &current);
51            (current, local, remote, status)
52        }
53    })
54    .await
55    .map_err(|e| ServerError::Internal(e.to_string()))?;
56
57    Ok(Json(serde_json::json!({
58        "current": current,
59        "local": local,
60        "remote": remote,
61        "status": status,
62    })))
63}
64
65#[derive(Debug, Deserialize)]
66#[serde(rename_all = "camelCase")]
67struct FetchBranchesBody {
68    repo_path: Option<String>,
69}
70
71async fn fetch_branches(
72    Json(body): Json<FetchBranchesBody>,
73) -> Result<Json<serde_json::Value>, ServerError> {
74    let repo_path = body
75        .repo_path
76        .ok_or_else(|| ServerError::BadRequest("Missing repoPath".into()))?;
77
78    if !std::path::Path::new(&repo_path).exists() {
79        return Err(ServerError::BadRequest(
80            "Missing or invalid repoPath".into(),
81        ));
82    }
83
84    let (current, local, remote, status) = tokio::task::spawn_blocking({
85        let rp = repo_path.clone();
86        move || {
87            git::fetch_remote(&rp);
88            let current = git::get_current_branch(&rp).unwrap_or_else(|| "unknown".into());
89            let local = git::list_local_branches(&rp);
90            let remote = git::list_remote_branches(&rp);
91            let status = git::get_branch_status(&rp, &current);
92            (current, local, remote, status)
93        }
94    })
95    .await
96    .map_err(|e| ServerError::Internal(e.to_string()))?;
97
98    Ok(Json(serde_json::json!({
99        "current": current,
100        "local": local,
101        "remote": remote,
102        "status": status,
103    })))
104}
105
106#[derive(Debug, Deserialize)]
107#[serde(rename_all = "camelCase")]
108struct CheckoutBody {
109    repo_path: Option<String>,
110    branch: Option<String>,
111    pull: Option<bool>,
112    action: Option<String>,
113}
114
115async fn checkout(Json(body): Json<CheckoutBody>) -> Result<Json<serde_json::Value>, ServerError> {
116    let repo_path = body
117        .repo_path
118        .ok_or_else(|| ServerError::BadRequest("Missing repoPath".into()))?;
119
120    if !std::path::Path::new(&repo_path).exists() {
121        return Err(ServerError::NotFound("Repository not found".into()));
122    }
123
124    if body.action.as_deref() == Some("reset") {
125        let (branch_info, status, repo_status) = tokio::task::spawn_blocking({
126            let rp = repo_path.clone();
127            move || {
128                git::reset_local_changes(&rp).map_err(ServerError::Internal)?;
129                let branch_info = git::get_branch_info(&rp);
130                let status = git::get_branch_status(&rp, &branch_info.current);
131                let repo_status = git::get_repo_status(&rp);
132                Ok::<_, ServerError>((branch_info, status, repo_status))
133            }
134        })
135        .await
136        .map_err(|e| ServerError::Internal(e.to_string()))??;
137
138        return Ok(Json(serde_json::json!({
139            "success": true,
140            "action": "reset",
141            "branch": branch_info.current,
142            "branches": branch_info.branches,
143            "status": status,
144            "repoStatus": repo_status,
145        })));
146    }
147
148    let branch = body
149        .branch
150        .ok_or_else(|| ServerError::BadRequest("Missing branch".into()))?;
151    let do_pull = body.pull.unwrap_or(false);
152
153    let (success, info, status) = tokio::task::spawn_blocking({
154        let rp = repo_path.clone();
155        let br = branch.clone();
156        move || {
157            let ok = git::checkout_branch(&rp, &br);
158            if ok && do_pull {
159                let _ = git::pull_branch(&rp);
160            }
161            let info = git::get_branch_info(&rp);
162            let status = git::get_branch_status(&rp, &info.current);
163            (ok, info, status)
164        }
165    })
166    .await
167    .map_err(|e| ServerError::Internal(e.to_string()))?;
168
169    if !success {
170        return Err(ServerError::Internal(format!(
171            "Failed to checkout branch '{branch}'"
172        )));
173    }
174
175    Ok(Json(serde_json::json!({
176        "success": true,
177        "branch": info.current,
178        "branches": info.branches,
179        "status": status,
180    })))
181}
182
183#[derive(Debug, Deserialize)]
184#[serde(rename_all = "camelCase")]
185struct DeleteBranchBody {
186    repo_path: Option<String>,
187    branch: Option<String>,
188}
189
190async fn delete_branch(
191    Json(body): Json<DeleteBranchBody>,
192) -> Result<Json<serde_json::Value>, ServerError> {
193    let repo_path = body
194        .repo_path
195        .ok_or_else(|| ServerError::BadRequest("Missing repoPath".into()))?;
196    let branch = body
197        .branch
198        .ok_or_else(|| ServerError::BadRequest("Missing branch".into()))?;
199
200    if !std::path::Path::new(&repo_path).exists() {
201        return Err(ServerError::NotFound("Repository not found".into()));
202    }
203
204    let branch_info = tokio::task::spawn_blocking({
205        let rp = repo_path.clone();
206        let br = branch.clone();
207        move || {
208            git::delete_branch(&rp, &br)?;
209            Ok::<_, String>(git::get_branch_info(&rp))
210        }
211    })
212    .await
213    .map_err(|e| ServerError::Internal(e.to_string()))?
214    .map_err(|message| {
215        if message.contains("current branch") {
216            ServerError::Conflict(message)
217        } else if message.contains("not found") {
218            ServerError::NotFound(message)
219        } else {
220            ServerError::Internal(message)
221        }
222    })?;
223
224    Ok(Json(serde_json::json!({
225        "success": true,
226        "deletedBranch": branch,
227        "current": branch_info.current,
228        "branches": branch_info.branches,
229    })))
230}