This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/ide/vessels/el-ide-server/src/api/git.rs
T
Will Anderson be013d2b42 rename crates/ → vessels/ — El's word for buildable units
Per the consolidation onto El: 'crates' is the Rust word, 'vessel' is
El's (per spec/language.md §15). The directory rename is the structural
marker that this slot holds an El buildable unit, even if its current
contents are still Rust pending port.

Mechanical: git mv crates vessels, sed workspace members and any path
dependencies, update CI workflow paths, update README references.
Cross-repo path dependencies (`../foo/crates/bar`) updated workspace-
wide so cargo metadata still resolves where the Rust still builds.
2026-04-30 15:34:20 -05:00

109 lines
3.1 KiB
Rust

//! Git API endpoints — status and diff.
use axum::{
extract::{Query, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
use crate::AppState;
type ApiResult<T> = Result<Json<T>, (StatusCode, Json<serde_json::Value>)>;
fn api_err(msg: impl std::fmt::Display) -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": msg.to_string() })),
)
}
// ── Types ─────────────────────────────────────────────────────────────────────
#[derive(Debug, Serialize)]
pub struct GitFileStatus {
pub path: String,
pub status: String, // "M", "A", "D", "?", "R", etc.
}
#[derive(Debug, Deserialize)]
pub struct DiffQuery {
pub path: Option<String>,
}
// ── Handlers ──────────────────────────────────────────────────────────────────
/// GET /api/git/status — returns list of changed files.
pub async fn git_status(
State(state): State<AppState>,
) -> ApiResult<Vec<GitFileStatus>> {
let project_path = &state.config.project_path;
let output = tokio::process::Command::new("git")
.args(["status", "--porcelain"])
.current_dir(project_path)
.output()
.await
.map_err(|e| api_err(format!("git status failed: {e}")))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut files = Vec::new();
for line in stdout.lines() {
if line.len() < 4 {
continue;
}
let xy = &line[..2];
let path = line[3..].trim();
// Handle renames: "old -> new"
let file_path = if let Some(arrow) = path.find(" -> ") {
&path[arrow + 4..]
} else {
path
};
// Determine status badge: prefer index status (first char)
let status = if xy.starts_with('M') || xy.ends_with('M') {
"M"
} else if xy.starts_with('A') || xy.starts_with('?') {
"A"
} else if xy.starts_with('D') || xy.ends_with('D') {
"D"
} else if xy.starts_with('R') {
"R"
} else {
"M"
};
files.push(GitFileStatus {
path: file_path.to_string(),
status: status.to_string(),
});
}
Ok(Json(files))
}
/// GET /api/git/diff?path=<file> — returns unified diff.
pub async fn git_diff(
State(state): State<AppState>,
Query(q): Query<DiffQuery>,
) -> ApiResult<String> {
let project_path = &state.config.project_path;
let mut cmd = tokio::process::Command::new("git");
cmd.arg("diff").current_dir(project_path);
if let Some(ref path) = q.path {
cmd.arg("--").arg(path);
}
let output = cmd
.output()
.await
.map_err(|e| api_err(format!("git diff failed: {e}")))?;
let diff = String::from_utf8_lossy(&output.stdout).to_string();
Ok(Json(diff))
}