use super::{parse_form, read_json_object, resolve_repo_cwd, string_field};
use crate::server::app::AppState;
use crate::server::errors::error;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{post, put};
use axum::{Json, Router};
use nomoreide_core::git_manager::GitManager;
use serde::Serialize;
use serde_json::Value;
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/api/git/fetch", post(fetch))
.route("/api/git/file", put(write_file))
.route("/api/git/commit", post(commit))
.route("/api/git/stage", post(stage))
.route("/api/git/unstage", post(unstage))
.route("/api/git/branches", post(create_branch))
.route("/api/git/branches/switch", post(switch_branch))
.route("/api/git/branches/delete", post(delete_branch))
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct OutputEnvelope {
ok: bool,
output: String,
}
async fn fetch(State(state): State<AppState>) -> Response {
let cwd = state.workspace_cwd().await;
match GitManager::fetch(&cwd).await {
Ok(output) => Json(OutputEnvelope { ok: true, output }).into_response(),
Err(reason) => error(StatusCode::INTERNAL_SERVER_ERROR, &reason.to_string()),
}
}
#[derive(Serialize)]
struct OkEnvelope {
ok: bool,
}
async fn write_file(State(state): State<AppState>, body: Bytes) -> Response {
let body = read_json_object(&body);
let path = string_field(&body, "path")
.map(str::trim)
.unwrap_or_default();
if path.is_empty() {
return error(StatusCode::BAD_REQUEST, "path is required");
}
let Some(content) = string_field(&body, "content") else {
return error(StatusCode::BAD_REQUEST, "content is required");
};
let cwd = state.workspace_cwd().await;
match GitManager::write_tracked_file(&cwd, path, content).await {
Ok(()) => Json(OkEnvelope { ok: true }).into_response(),
Err(reason) => error(StatusCode::BAD_REQUEST, &reason.to_string()),
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CommitEnvelope {
ok: bool,
output: String,
author: Option<nomoreide_core::config::GithubIdentityDef>,
}
async fn commit(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let repo = form.get("repo").map(String::as_str);
let (cwd, repository) = match resolve_repo_cwd(&state, repo).await {
Ok(resolved) => resolved,
Err(response) => return response,
};
let Some(message) = form
.get("message")
.map(|value| value.trim())
.filter(|value| !value.is_empty())
else {
return error(StatusCode::BAD_REQUEST, "message is required");
};
let config = match state.config_store.load().await {
Ok(config) => config,
Err(reason) => return error(StatusCode::BAD_REQUEST, &reason.to_string()),
};
let identity = nomoreide_core::git_identity::resolve_identity_state(
&state.config_store,
&config,
repository.as_ref(),
&cwd,
)
.await;
match GitManager::commit(&cwd, message, identity.selected.as_ref()).await {
Ok(output) => Json(CommitEnvelope {
ok: true,
output,
author: identity.selected,
})
.into_response(),
Err(reason) => error(StatusCode::BAD_REQUEST, &reason.to_string()),
}
}
async fn stage(State(state): State<AppState>, body: Bytes) -> Response {
index_move(state, &body, true).await
}
async fn unstage(State(state): State<AppState>, body: Bytes) -> Response {
index_move(state, &body, false).await
}
async fn index_move(state: AppState, body: &Bytes, staging: bool) -> Response {
let body = read_json_object(body);
let (cwd, _repository) = match resolve_repo_cwd(&state, string_field(&body, "repo")).await {
Ok(resolved) => resolved,
Err(response) => return response,
};
let paths: Vec<String> = body
.get("paths")
.and_then(Value::as_array)
.map(|values| {
values
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
let result = if staging {
GitManager::stage(&cwd, &paths).await
} else {
GitManager::unstage(&cwd, &paths).await
};
match result {
Ok(output) => Json(OutputEnvelope { ok: true, output }).into_response(),
Err(reason) => error(StatusCode::BAD_REQUEST, &reason.to_string()),
}
}
async fn switch_branch(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let Ok(name) = required(&form, "name") else {
return error(StatusCode::INTERNAL_SERVER_ERROR, "name is required");
};
let cwd = state.workspace_cwd().await;
match GitManager::switch_branch(&cwd, &name).await {
Ok(output) => Json(OutputEnvelope { ok: true, output }).into_response(),
Err(reason) => error(StatusCode::INTERNAL_SERVER_ERROR, &reason.to_string()),
}
}
async fn create_branch(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let Ok(name) = required(&form, "name") else {
return error(StatusCode::INTERNAL_SERVER_ERROR, "name is required");
};
let start_point = form
.get("startPoint")
.map(|value| value.trim())
.filter(|value| !value.is_empty());
let cwd = state.workspace_cwd().await;
match GitManager::create_branch(&cwd, &name, start_point).await {
Ok(output) => Json(OutputEnvelope { ok: true, output }).into_response(),
Err(reason) => error(StatusCode::INTERNAL_SERVER_ERROR, &reason.to_string()),
}
}
async fn delete_branch(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let cwd = match resolve_repo_cwd(&state, form.get("repo").map(String::as_str)).await {
Ok((cwd, _)) => cwd,
Err(response) => return response,
};
let name = match required(&form, "name") {
Ok(name) => name,
Err(reason) => return error(StatusCode::BAD_REQUEST, &reason),
};
match GitManager::delete_branch(&cwd, &name).await {
Ok(output) => Json(OutputEnvelope { ok: true, output }).into_response(),
Err(reason) => error(StatusCode::BAD_REQUEST, &reason.to_string()),
}
}
fn required(form: &std::collections::HashMap<String, String>, key: &str) -> Result<String, String> {
form.get(key)
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| format!("{key} is required"))
}