use super::{parse_form, read_json_object};
use crate::server::app::AppState;
use crate::server::errors::{config_failure, error};
use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post, put};
use axum::{Json, Router};
use nomoreide_core::config::{selected_git_repository, Config, GitRepoDef};
use serde::Serialize;
use serde_json::Value;
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/api/repositories", get(list))
.route("/api/git/repositories", post(register))
.route("/api/git/repositories/:name", delete(remove))
.route("/api/git/clone", post(clone))
.route("/api/git/adopt", post(adopt))
.route("/api/git/create", post(create))
.route("/api/git/select", post(select))
.route("/api/git/board", put(board))
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RepositoryEntry {
name: String,
selected: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RepositoryList {
ok: bool,
repositories: Vec<RepositoryEntry>,
}
async fn list(State(state): State<AppState>) -> Response {
let config = match state.config_store.load().await {
Ok(config) => config,
Err(_) => {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not read the configuration",
)
}
};
let selected = selected_git_repository(&config).map(|repository| repository.name.clone());
Json(RepositoryList {
ok: true,
repositories: config
.git_repositories
.iter()
.map(|repository| RepositoryEntry {
selected: Some(&repository.name) == selected.as_ref(),
name: repository.name.clone(),
})
.collect(),
})
.into_response()
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConfigEnvelope {
ok: bool,
config: Value,
}
fn config_envelope(config: &Config) -> Response {
Json(ConfigEnvelope {
ok: true,
config: config.public_value(),
})
.into_response()
}
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"))
}
async fn register(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let (name, path) = match (required(&form, "name"), required(&form, "path")) {
(Ok(name), Ok(path)) => (name, path),
(Err(reason), _) | (_, Err(reason)) => {
return error(StatusCode::INTERNAL_SERVER_ERROR, &reason)
}
};
match state
.config_store
.register_git_repository(repository(name, path))
.await
{
Ok(config) => config_envelope(&config),
Err(reason) => config_failure(&reason),
}
}
fn repository(name: String, path: String) -> GitRepoDef {
GitRepoDef {
name,
path,
active_worktree_path: None,
github_credential: None,
provider_projects: None,
legacy_vercel_project_id: None,
}
}
async fn remove(State(state): State<AppState>, Path(name): Path<String>) -> Response {
match state.config_store.remove_git_repository(&name).await {
Ok(config) => config_envelope(&config),
Err(reason) => config_failure(&reason),
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct OnboardedEnvelope {
ok: bool,
name: String,
path: String,
}
async fn clone(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let url = match required(&form, "url") {
Ok(url) => url,
Err(reason) => return error(StatusCode::INTERNAL_SERVER_ERROR, &reason),
};
let config = match state.config_store.load().await {
Ok(config) => config,
Err(reason) => return error(StatusCode::UNPROCESSABLE_ENTITY, &reason.to_string()),
};
let token = state
.config_store
.get_github_token(&config, "github.com")
.map(str::to_string);
let cloned = nomoreide_core::repo_onboard::clone_repository(&url, None, token.as_deref()).await;
let cloned = match cloned {
Ok(cloned) => cloned,
Err(reason) => return error(StatusCode::UNPROCESSABLE_ENTITY, &reason.to_string()),
};
if let Err(reason) = state
.config_store
.register_git_repository(repository(cloned.name.clone(), cloned.clone_path.clone()))
.await
{
return error(StatusCode::UNPROCESSABLE_ENTITY, &reason.to_string());
}
Json(OnboardedEnvelope {
ok: true,
name: cloned.name,
path: cloned.clone_path,
})
.into_response()
}
async fn adopt(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let from = match required(&form, "path") {
Ok(path) => path,
Err(reason) => return error(StatusCode::INTERNAL_SERVER_ERROR, &reason),
};
let Some(root) = nomoreide_core::repo_match::git_toplevel(&from).await else {
return error(
StatusCode::UNPROCESSABLE_ENTITY,
&format!("Not inside a Git repository: {from}"),
);
};
let name = root
.split('/')
.rfind(|segment| !segment.is_empty())
.unwrap_or(&root)
.to_string();
match state
.config_store
.register_git_repository(repository(name.clone(), root.clone()))
.await
{
Ok(_) => Json(OnboardedEnvelope {
ok: true,
name,
path: root,
})
.into_response(),
Err(reason) => error(StatusCode::UNPROCESSABLE_ENTITY, &reason.to_string()),
}
}
async fn create(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let name = match required(&form, "name") {
Ok(name) => name,
Err(reason) => return error(StatusCode::UNPROCESSABLE_ENTITY, &reason),
};
let parent = form
.get("parentPath")
.map(|value| value.trim())
.filter(|value| !value.is_empty());
let created = match nomoreide_core::repo_create::create_repository(&name, parent).await {
Ok(created) => created,
Err(reason) => return error(StatusCode::UNPROCESSABLE_ENTITY, &reason.to_string()),
};
if let Err(reason) = state
.config_store
.register_git_repository(repository(created.name.clone(), created.path.clone()))
.await
{
return error(StatusCode::UNPROCESSABLE_ENTITY, &reason.to_string());
}
Json(OnboardedEnvelope {
ok: true,
name: created.name,
path: created.path,
})
.into_response()
}
async fn select(State(state): State<AppState>, body: Bytes) -> Response {
let form = parse_form(&body);
let name = match required(&form, "name") {
Ok(name) => name,
Err(reason) => return error(StatusCode::INTERNAL_SERVER_ERROR, &reason),
};
match state.config_store.select_git_repository(Some(name)).await {
Ok(config) => config_envelope(&config),
Err(reason) => config_failure(&reason),
}
}
async fn board(State(state): State<AppState>, body: Bytes) -> Response {
let body = read_json_object(&body);
let Some(names) = body.get("names").and_then(Value::as_array) else {
return error(StatusCode::BAD_REQUEST, "names array is required");
};
let names: Vec<String> = names
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect();
match state.config_store.set_git_board_repositories(names).await {
Ok(config) => Json(BoardEnvelope {
ok: true,
board: config.git_board_repositories.unwrap_or_default(),
})
.into_response(),
Err(reason) => config_failure(&reason),
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct BoardEnvelope {
ok: bool,
board: Vec<String>,
}