Archived
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.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
//! 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))
|
||||
}
|
||||
Reference in New Issue
Block a user