routa_server/api/
clone_branches.rs1use 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, ¤t);
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, ¤t);
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}