Archived
be013d2b42
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.
109 lines
3.1 KiB
Rust
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))
|
|
}
|